[Feature] Enable response type selection for oauth2 implicit mode (#681)

* Enable response type selection for oauth2 implicit

* Parse token even if callback server is unreachable

* Fix style: add _ prefix to private method

* Fix style: reorder private method declaration

* Set OAuth 2.0 default responseType value to token

* Add responseType to params

* Fix response type constant value

* Code styling

* Fix authorization request parameters

* Don't open dev tools
This commit is contained in:
Emanuel Fonseca 2018-01-16 06:08:46 +00:00 committed by Gregory Schier
parent dd51487905
commit 3f46f5898c
6 changed files with 63 additions and 16 deletions

View File

@ -30,7 +30,8 @@ describe('implicit', () => {
state: STATE,
error: null,
error_description: null,
error_uri: null
error_uri: null,
nonce: null
});
});
});

View File

@ -5,13 +5,16 @@ export const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials';
export const GRANT_TYPE_REFRESH = 'refresh_token';
export const RESPONSE_TYPE_CODE = 'code';
export const RESPONSE_TYPE_TOKEN = 'token';
export const RESPONSE_TYPE_ID = 'id_token';
export const RESPONSE_TYPE_ACCESS = 'token';
export const RESPONSE_TYPE_BOTH = 'id_token token';
export const P_ACCESS_TOKEN = 'access_token';
export const P_CLIENT_ID = 'client_id';
export const P_CLIENT_SECRET = 'client_secret';
export const P_AUDIENCE = 'audience';
export const P_CODE = 'code';
export const P_NONCE = 'nonce';
export const P_ERROR = 'error';
export const P_ERROR_DESCRIPTION = 'error_description';
export const P_ERROR_URI = 'error_uri';

View File

@ -94,6 +94,7 @@ async function _getOAuth2ImplicitHeader (
requestId,
authentication.authorizationUrl,
authentication.clientId,
authentication.responseType,
authentication.redirectUrl,
authentication.scope,
authentication.state

View File

@ -7,16 +7,21 @@ export default async function (
requestId: string,
authorizationUrl: string,
clientId: string,
responseType: string = c.RESPONSE_TYPE_ACCESS,
redirectUri: string = '',
scope: string = '',
state: string = ''
): Promise<Object> {
const params = [
{name: c.P_RESPONSE_TYPE, value: c.RESPONSE_TYPE_TOKEN},
{name: c.P_RESPONSE_TYPE, value: responseType},
{name: c.P_CLIENT_ID, value: clientId}
];
// Add optional params
if (responseType === c.RESPONSE_TYPE_BOTH) {
const responseNonce: string = ((Math.floor(Math.random() * 9999999999999) + 1): any);
params.push({name: c.P_NONCE, value: responseNonce});
}
redirectUri && params.push({name: c.P_REDIRECT_URI, value: redirectUri});
scope && params.push({name: c.P_SCOPE, value: scope});
state && params.push({name: c.P_STATE, value: state});
@ -25,7 +30,7 @@ export default async function (
const qs = buildQueryStringFromParams(params);
const finalUrl = joinUrlAndQueryString(authorizationUrl, qs);
const redirectedTo = await authorizeUserInWindow(finalUrl, /(access_token=|error=)/);
const redirectedTo = await authorizeUserInWindow(finalUrl, /(access_token=)/, /(error=)/);
const fragment = redirectedTo.split('#')[1];
if (fragment) {
@ -33,6 +38,7 @@ export default async function (
c.P_ACCESS_TOKEN,
c.P_TOKEN_TYPE,
c.P_EXPIRES_IN,
c.P_NONCE,
c.P_SCOPE,
c.P_STATE,
c.P_ERROR,

View File

@ -27,9 +27,27 @@ export function responseToObject (body, keys) {
return results;
}
export function authorizeUserInWindow (url, urlRegex = /.*/) {
export function authorizeUserInWindow (url, urlSuccessRegex = /.*/, urlFailureRegex = /.*/) {
return new Promise((resolve, reject) => {
let finalUrl = null;
let hasError = false;
function _parseUrl (currentUrl) {
if (currentUrl.match(urlSuccessRegex)) {
console.log(`[oauth2] Matched redirect to "${currentUrl}" with ${urlSuccessRegex.toString()}`);
finalUrl = currentUrl;
child.close();
} else if (currentUrl.match(urlFailureRegex)) {
console.log(`[oauth2] Matched redirect to "${currentUrl}" with ${urlFailureRegex.toString()}`);
hasError = true;
child.close();
} else if (currentUrl === url) {
// It's the first one, so it's not a redirect
console.log(`[oauth2] Loaded "${currentUrl}"`);
} else {
console.log(`[oauth2] Ignoring URL "${currentUrl}". Didn't match ${urlSuccessRegex.toString()}`);
}
}
// Create a child window
const child = new electron.remote.BrowserWindow({
@ -45,7 +63,11 @@ export function authorizeUserInWindow (url, urlRegex = /.*/) {
if (finalUrl) {
resolve(finalUrl);
} else {
reject(new Error('Authorization window closed'));
let errorDescription = 'Authorization window closed';
if (hasError) {
errorDescription += ' after oauth error';
}
reject(new Error(errorDescription));
}
});
@ -53,16 +75,12 @@ export function authorizeUserInWindow (url, urlRegex = /.*/) {
child.webContents.on('did-navigate', () => {
// Be sure to resolve URL so that we can handle redirects with no host like /foo/bar
const currentUrl = child.webContents.getURL();
if (currentUrl.match(urlRegex)) {
console.log(`[oauth2] Matched redirect to "${currentUrl}" with ${urlRegex.toString()}`);
finalUrl = currentUrl;
child.close();
} else if (currentUrl === url) {
// It's the first one, so it's not a redirect
console.log(`[oauth2] Loaded "${currentUrl}"`);
} else {
console.log(`[oauth2] Ignoring URL "${currentUrl}". Didn't match ${urlRegex.toString()}`);
}
_parseUrl(currentUrl);
});
child.webContents.on('did-fail-load', (e, errorCode, errorDescription, url) => {
// Listen for did-fail-load to be able to parse the URL even when the callback server is unreachable
_parseUrl(url);
});
// Show the window to the user after it loads

View File

@ -8,6 +8,7 @@ import autobind from 'autobind-decorator';
import OneLineEditor from '../../codemirror/one-line-editor';
import * as misc from '../../../../common/misc';
import {GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_CLIENT_CREDENTIALS, GRANT_TYPE_IMPLICIT, GRANT_TYPE_PASSWORD} from '../../../../network/o-auth-2/constants';
import {RESPONSE_TYPE_ID, RESPONSE_TYPE_ACCESS, RESPONSE_TYPE_BOTH} from '../../../../network/o-auth-2/constants';
import authorizationUrls from '../../../../datasets/authorization-urls';
import accessTokenUrls from '../../../../datasets/access-token-urls';
import getAccessToken from '../../../../network/o-auth-2/get-token';
@ -115,6 +116,10 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
this.props.onChange(authentication);
}
_handlerChangeResponseType (e: SyntheticEvent<HTMLInputElement>): void {
this._handleChangeProperty('responseType', e.currentTarget.value);
}
_handleChangeClientId (value: string): void {
this._handleChangeProperty('clientId', value);
}
@ -310,6 +315,18 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
'Change Authorization header prefix from Bearer to something else'
);
const responseType = this.renderSelectRow(
'Response Type',
'responseType',
[
{name: 'Access Token', value: RESPONSE_TYPE_ACCESS},
{name: 'ID Token', value: RESPONSE_TYPE_ID},
{name: 'ID and Access Token', value: RESPONSE_TYPE_BOTH}
],
this._handlerChangeResponseType,
'Indicates the type of credentials returned in the response'
);
const audience = this.renderInputRow(
'Audience',
'audience',
@ -378,6 +395,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
];
advancedFields = [
responseType,
scope,
state,
tokenPrefix