[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, state: STATE,
error: null, error: null,
error_description: 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 GRANT_TYPE_REFRESH = 'refresh_token';
export const RESPONSE_TYPE_CODE = 'code'; 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_ACCESS_TOKEN = 'access_token';
export const P_CLIENT_ID = 'client_id'; export const P_CLIENT_ID = 'client_id';
export const P_CLIENT_SECRET = 'client_secret'; export const P_CLIENT_SECRET = 'client_secret';
export const P_AUDIENCE = 'audience'; export const P_AUDIENCE = 'audience';
export const P_CODE = 'code'; export const P_CODE = 'code';
export const P_NONCE = 'nonce';
export const P_ERROR = 'error'; export const P_ERROR = 'error';
export const P_ERROR_DESCRIPTION = 'error_description'; export const P_ERROR_DESCRIPTION = 'error_description';
export const P_ERROR_URI = 'error_uri'; export const P_ERROR_URI = 'error_uri';

View File

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

View File

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

View File

@ -27,9 +27,27 @@ export function responseToObject (body, keys) {
return results; return results;
} }
export function authorizeUserInWindow (url, urlRegex = /.*/) { export function authorizeUserInWindow (url, urlSuccessRegex = /.*/, urlFailureRegex = /.*/) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let finalUrl = null; 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 // Create a child window
const child = new electron.remote.BrowserWindow({ const child = new electron.remote.BrowserWindow({
@ -45,7 +63,11 @@ export function authorizeUserInWindow (url, urlRegex = /.*/) {
if (finalUrl) { if (finalUrl) {
resolve(finalUrl); resolve(finalUrl);
} else { } 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', () => { child.webContents.on('did-navigate', () => {
// Be sure to resolve URL so that we can handle redirects with no host like /foo/bar // Be sure to resolve URL so that we can handle redirects with no host like /foo/bar
const currentUrl = child.webContents.getURL(); const currentUrl = child.webContents.getURL();
if (currentUrl.match(urlRegex)) { _parseUrl(currentUrl);
console.log(`[oauth2] Matched redirect to "${currentUrl}" with ${urlRegex.toString()}`); });
finalUrl = currentUrl;
child.close(); child.webContents.on('did-fail-load', (e, errorCode, errorDescription, url) => {
} else if (currentUrl === url) { // Listen for did-fail-load to be able to parse the URL even when the callback server is unreachable
// It's the first one, so it's not a redirect _parseUrl(url);
console.log(`[oauth2] Loaded "${currentUrl}"`);
} else {
console.log(`[oauth2] Ignoring URL "${currentUrl}". Didn't match ${urlRegex.toString()}`);
}
}); });
// Show the window to the user after it loads // 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 OneLineEditor from '../../codemirror/one-line-editor';
import * as misc from '../../../../common/misc'; 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 {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 authorizationUrls from '../../../../datasets/authorization-urls';
import accessTokenUrls from '../../../../datasets/access-token-urls'; import accessTokenUrls from '../../../../datasets/access-token-urls';
import getAccessToken from '../../../../network/o-auth-2/get-token'; import getAccessToken from '../../../../network/o-auth-2/get-token';
@ -115,6 +116,10 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
this.props.onChange(authentication); this.props.onChange(authentication);
} }
_handlerChangeResponseType (e: SyntheticEvent<HTMLInputElement>): void {
this._handleChangeProperty('responseType', e.currentTarget.value);
}
_handleChangeClientId (value: string): void { _handleChangeClientId (value: string): void {
this._handleChangeProperty('clientId', value); this._handleChangeProperty('clientId', value);
} }
@ -310,6 +315,18 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
'Change Authorization header prefix from Bearer to something else' '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( const audience = this.renderInputRow(
'Audience', 'Audience',
'audience', 'audience',
@ -378,6 +395,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
]; ];
advancedFields = [ advancedFields = [
responseType,
scope, scope,
state, state,
tokenPrefix tokenPrefix