Add resource param to client credential oauth grant. (#1174)

Addresses #716
This commit is contained in:
Devin Carr 2018-09-26 16:03:40 -07:00 committed by Gregory Schier
parent a921c9c054
commit 9413481b2e
5 changed files with 76 additions and 135 deletions

View File

@ -10,6 +10,8 @@ const ACCESS_TOKEN_URL = 'https://foo.com/access_token';
const CLIENT_ID = 'client_123'; const CLIENT_ID = 'client_123';
const CLIENT_SECRET = 'secret_12345456677756343'; const CLIENT_SECRET = 'secret_12345456677756343';
const SCOPE = 'scope_123'; const SCOPE = 'scope_123';
const AUDIENCE = 'https://foo.com/userinfo';
const RESOURCE = 'https://foo.com/resource';
describe('client_credentials', () => { describe('client_credentials', () => {
beforeEach(globalBeforeEach); beforeEach(globalBeforeEach);
@ -21,7 +23,9 @@ describe('client_credentials', () => {
JSON.stringify({ JSON.stringify({
access_token: 'token_123', access_token: 'token_123',
token_type: 'token_type', token_type: 'token_type',
scope: SCOPE scope: SCOPE,
audience: AUDIENCE,
resource: RESOURCE
}) })
); );
@ -39,7 +43,9 @@ describe('client_credentials', () => {
false, false,
CLIENT_ID, CLIENT_ID,
CLIENT_SECRET, CLIENT_SECRET,
SCOPE SCOPE,
AUDIENCE,
RESOURCE
); );
// Check the request to fetch the token // Check the request to fetch the token
@ -53,7 +59,9 @@ describe('client_credentials', () => {
mimeType: 'application/x-www-form-urlencoded', mimeType: 'application/x-www-form-urlencoded',
params: [ params: [
{ name: 'grant_type', value: 'client_credentials' }, { name: 'grant_type', value: 'client_credentials' },
{ name: 'scope', value: SCOPE } { name: 'scope', value: SCOPE },
{ name: 'audience', value: AUDIENCE },
{ name: 'resource', value: RESOURCE }
] ]
}, },
headers: [ headers: [
@ -80,6 +88,8 @@ describe('client_credentials', () => {
expires_in: null, expires_in: null,
token_type: 'token_type', token_type: 'token_type',
scope: SCOPE, scope: SCOPE,
audience: AUDIENCE,
resource: RESOURCE,
error: null, error: null,
error_uri: null, error_uri: null,
error_description: null, error_description: null,
@ -95,7 +105,9 @@ describe('client_credentials', () => {
JSON.stringify({ JSON.stringify({
access_token: 'token_123', access_token: 'token_123',
token_type: 'token_type', token_type: 'token_type',
scope: SCOPE scope: SCOPE,
audience: AUDIENCE,
resource: RESOURCE
}) })
); );
@ -104,9 +116,7 @@ describe('client_credentials', () => {
bodyCompression: '', bodyCompression: '',
parentId: 'req_1', parentId: 'req_1',
statusCode: 200, statusCode: 200,
headers: [ headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }]
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }
]
})); }));
const result = await getToken( const result = await getToken(
@ -115,7 +125,9 @@ describe('client_credentials', () => {
true, true,
CLIENT_ID, CLIENT_ID,
CLIENT_SECRET, CLIENT_SECRET,
SCOPE SCOPE,
AUDIENCE,
RESOURCE
); );
// Check the request to fetch the token // Check the request to fetch the token
@ -130,6 +142,8 @@ describe('client_credentials', () => {
params: [ params: [
{ name: 'grant_type', value: 'client_credentials' }, { name: 'grant_type', value: 'client_credentials' },
{ name: 'scope', value: SCOPE }, { name: 'scope', value: SCOPE },
{ name: 'audience', value: AUDIENCE },
{ name: 'resource', value: RESOURCE },
{ name: 'client_id', value: CLIENT_ID }, { name: 'client_id', value: CLIENT_ID },
{ name: 'client_secret', value: CLIENT_SECRET } { name: 'client_secret', value: CLIENT_SECRET }
] ]
@ -154,6 +168,8 @@ describe('client_credentials', () => {
expires_in: null, expires_in: null,
token_type: 'token_type', token_type: 'token_type',
scope: SCOPE, scope: SCOPE,
audience: AUDIENCE,
resource: RESOURCE,
error: null, error: null,
error_uri: null, error_uri: null,
error_description: null, error_description: null,

View File

@ -13,6 +13,7 @@ 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_RESOURCE = 'resource';
export const P_CODE = 'code'; export const P_CODE = 'code';
export const P_NONCE = 'nonce'; export const P_NONCE = 'nonce';
export const P_ERROR = 'error'; export const P_ERROR = 'error';

View File

@ -30,17 +30,9 @@ export default async function(
): Promise<OAuth2Token | null> { ): Promise<OAuth2Token | null> {
switch (authentication.grantType) { switch (authentication.grantType) {
case GRANT_TYPE_AUTHORIZATION_CODE: case GRANT_TYPE_AUTHORIZATION_CODE:
return _getOAuth2AuthorizationCodeHeader( return _getOAuth2AuthorizationCodeHeader(requestId, authentication, forceRefresh);
requestId,
authentication,
forceRefresh
);
case GRANT_TYPE_CLIENT_CREDENTIALS: case GRANT_TYPE_CLIENT_CREDENTIALS:
return _getOAuth2ClientCredentialsHeader( return _getOAuth2ClientCredentialsHeader(requestId, authentication, forceRefresh);
requestId,
authentication,
forceRefresh
);
case GRANT_TYPE_IMPLICIT: case GRANT_TYPE_IMPLICIT:
return _getOAuth2ImplicitHeader(requestId, authentication, forceRefresh); return _getOAuth2ImplicitHeader(requestId, authentication, forceRefresh);
case GRANT_TYPE_PASSWORD: case GRANT_TYPE_PASSWORD:
@ -55,11 +47,7 @@ async function _getOAuth2AuthorizationCodeHeader(
authentication: RequestAuthentication, authentication: RequestAuthentication,
forceRefresh: boolean forceRefresh: boolean
): Promise<OAuth2Token | null> { ): Promise<OAuth2Token | null> {
const oAuth2Token = await _getAccessToken( const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
requestId,
authentication,
forceRefresh
);
if (oAuth2Token) { if (oAuth2Token) {
return oAuth2Token; return oAuth2Token;
@ -85,11 +73,7 @@ async function _getOAuth2ClientCredentialsHeader(
authentication: RequestAuthentication, authentication: RequestAuthentication,
forceRefresh: boolean forceRefresh: boolean
): Promise<OAuth2Token | null> { ): Promise<OAuth2Token | null> {
const oAuth2Token = await _getAccessToken( const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
requestId,
authentication,
forceRefresh
);
if (oAuth2Token) { if (oAuth2Token) {
return oAuth2Token; return oAuth2Token;
@ -102,7 +86,8 @@ async function _getOAuth2ClientCredentialsHeader(
authentication.clientId, authentication.clientId,
authentication.clientSecret, authentication.clientSecret,
authentication.scope, authentication.scope,
authentication.audience authentication.audience,
authentication.resource
); );
return _updateOAuth2Token(requestId, results); return _updateOAuth2Token(requestId, results);
@ -113,11 +98,7 @@ async function _getOAuth2ImplicitHeader(
authentication: RequestAuthentication, authentication: RequestAuthentication,
forceRefresh: boolean forceRefresh: boolean
): Promise<OAuth2Token | null> { ): Promise<OAuth2Token | null> {
const oAuth2Token = await _getAccessToken( const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
requestId,
authentication,
forceRefresh
);
if (oAuth2Token) { if (oAuth2Token) {
return oAuth2Token; return oAuth2Token;
@ -142,11 +123,7 @@ async function _getOAuth2PasswordHeader(
authentication: RequestAuthentication, authentication: RequestAuthentication,
forceRefresh: boolean forceRefresh: boolean
): Promise<OAuth2Token | null> { ): Promise<OAuth2Token | null> {
const oAuth2Token = await _getAccessToken( const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
requestId,
authentication,
forceRefresh
);
if (oAuth2Token) { if (oAuth2Token) {
return oAuth2Token; return oAuth2Token;
@ -175,9 +152,7 @@ async function _getAccessToken(
// See if we have a token already // // See if we have a token already //
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
let token: OAuth2Token | null = await models.oAuth2Token.getByParentId( let token: OAuth2Token | null = await models.oAuth2Token.getByParentId(requestId);
requestId
);
if (!token) { if (!token) {
return null; return null;
@ -222,10 +197,7 @@ async function _getAccessToken(
return _updateOAuth2Token(requestId, refreshResults); return _updateOAuth2Token(requestId, refreshResults);
} }
async function _updateOAuth2Token( async function _updateOAuth2Token(requestId: string, authResults: Object): Promise<OAuth2Token> {
requestId: string,
authResults: Object
): Promise<OAuth2Token> {
const oAuth2Token = await models.oAuth2Token.getOrCreateByParentId(requestId); const oAuth2Token = await models.oAuth2Token.getOrCreateByParentId(requestId);
// Calculate expiry date // Calculate expiry date

View File

@ -13,15 +13,15 @@ export default async function(
clientId: string, clientId: string,
clientSecret: string, clientSecret: string,
scope: string = '', scope: string = '',
audience: string = '' audience: string = '',
resource: string = ''
): Promise<Object> { ): Promise<Object> {
const params = [ const params = [{ name: c.P_GRANT_TYPE, value: c.GRANT_TYPE_CLIENT_CREDENTIALS }];
{ name: c.P_GRANT_TYPE, value: c.GRANT_TYPE_CLIENT_CREDENTIALS }
];
// Add optional params // Add optional params
scope && params.push({ name: c.P_SCOPE, value: scope }); scope && params.push({ name: c.P_SCOPE, value: scope });
audience && params.push({ name: c.P_AUDIENCE, value: audience }); audience && params.push({ name: c.P_AUDIENCE, value: audience });
resource && params.push({ name: c.P_RESOURCE, value: resource });
const headers = [ const headers = [
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, { name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
@ -70,6 +70,8 @@ export default async function(
c.P_TOKEN_TYPE, c.P_TOKEN_TYPE,
c.P_EXPIRES_IN, c.P_EXPIRES_IN,
c.P_SCOPE, c.P_SCOPE,
c.P_AUDIENCE,
c.P_RESOURCE,
c.P_ERROR, c.P_ERROR,
c.P_ERROR_URI, c.P_ERROR_URI,
c.P_ERROR_DESCRIPTION c.P_ERROR_DESCRIPTION

View File

@ -71,9 +71,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
this.setState({ showAdvanced }); this.setState({ showAdvanced });
} }
async _handleUpdateAccessToken( async _handleUpdateAccessToken(e: SyntheticEvent<HTMLInputElement>): Promise<void> {
e: SyntheticEvent<HTMLInputElement>
): Promise<void> {
const { oAuth2Token } = this.props; const { oAuth2Token } = this.props;
const accessToken = e.currentTarget.value; const accessToken = e.currentTarget.value;
@ -87,9 +85,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
} }
} }
async _handleUpdateRefreshToken( async _handleUpdateRefreshToken(e: SyntheticEvent<HTMLInputElement>): Promise<void> {
e: SyntheticEvent<HTMLInputElement>
): Promise<void> {
const { oAuth2Token } = this.props; const { oAuth2Token } = this.props;
const refreshToken = e.currentTarget.value; const refreshToken = e.currentTarget.value;
@ -104,9 +100,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
} }
async _handleClearTokens(): Promise<void> { async _handleClearTokens(): Promise<void> {
const oAuth2Token = await models.oAuth2Token.getByParentId( const oAuth2Token = await models.oAuth2Token.getByParentId(this.props.request._id);
this.props.request._id
);
if (oAuth2Token) { if (oAuth2Token) {
await models.oAuth2Token.remove(oAuth2Token); await models.oAuth2Token.remove(oAuth2Token);
} }
@ -129,9 +123,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
const { request } = this.props; const { request } = this.props;
try { try {
const authentication = await this.props.handleRender( const authentication = await this.props.handleRender(request.authentication);
request.authentication
);
await getAccessToken(request._id, authentication, true); await getAccessToken(request._id, authentication, true);
this.setState({ loading: false }); this.setState({ loading: false });
} catch (err) { } catch (err) {
@ -157,10 +149,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
} }
_handleChangeCredentialsInBody(e: SyntheticEvent<HTMLInputElement>): void { _handleChangeCredentialsInBody(e: SyntheticEvent<HTMLInputElement>): void {
this._handleChangeProperty( this._handleChangeProperty('credentialsInBody', e.currentTarget.value === 'true');
'credentialsInBody',
e.currentTarget.value === 'true'
);
} }
_handleChangeEnabled(value: boolean): void { _handleChangeEnabled(value: boolean): void {
@ -207,6 +196,10 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
this._handleChangeProperty('audience', value); this._handleChangeProperty('audience', value);
} }
_handleChangeResource(value: string): void {
this._handleChangeProperty('resource', value);
}
_handleChangeGrantType(e: SyntheticEvent<HTMLInputElement>): void { _handleChangeGrantType(e: SyntheticEvent<HTMLInputElement>): void {
this._handleChangeProperty('grantType', e.currentTarget.value); this._handleChangeProperty('grantType', e.currentTarget.value);
} }
@ -248,18 +241,10 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
help: string | null = null, help: string | null = null,
handleAutocomplete: Function | null = null handleAutocomplete: Function | null = null
): React.Element<*> { ): React.Element<*> {
const { const { handleRender, handleGetRenderContext, request, nunjucksPowerUserMode } = this.props;
handleRender,
handleGetRenderContext,
request,
nunjucksPowerUserMode
} = this.props;
const { authentication } = request; const { authentication } = request;
const id = label.replace(/ /g, '-'); const id = label.replace(/ /g, '-');
const type = const type = !this.props.showPasswords && property === 'password' ? 'password' : 'text';
!this.props.showPasswords && property === 'password'
? 'password'
: 'text';
return ( return (
<tr key={id}> <tr key={id}>
<td className="pad-right no-wrap valign-middle"> <td className="pad-right no-wrap valign-middle">
@ -270,12 +255,9 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
</td> </td>
<td className="wide"> <td className="wide">
<div <div
className={classnames( className={classnames('form-control form-control--underlined no-margin', {
'form-control form-control--underlined no-margin', 'form-control--inactive': authentication.disabled
{ })}>
'form-control--inactive': authentication.disabled
}
)}>
<OneLineEditor <OneLineEditor
id={id} id={id}
type={type} type={type}
@ -316,12 +298,9 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
</td> </td>
<td className="wide"> <td className="wide">
<div <div
className={classnames( className={classnames('form-control form-control--outlined no-margin', {
'form-control form-control--outlined no-margin', 'form-control--inactive': authentication.disabled
{ })}>
'form-control--inactive': authentication.disabled
}
)}>
<select id={id} onChange={onChange} value={value}> <select id={id} onChange={onChange} value={value}>
{options.map(({ name, value }) => ( {options.map(({ name, value }) => (
<option key={value} value={value}> <option key={value} value={value}>
@ -341,11 +320,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
let basicFields = []; let basicFields = [];
let advancedFields = []; let advancedFields = [];
const clientId = this.renderInputRow( const clientId = this.renderInputRow('Client ID', 'clientId', this._handleChangeClientId);
'Client ID',
'clientId',
this._handleChangeClientId
);
const clientSecret = this.renderInputRow( const clientSecret = this.renderInputRow(
'Client Secret', 'Client Secret',
@ -378,29 +353,13 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
'redirected URL' 'redirected URL'
); );
const state = this.renderInputRow( const state = this.renderInputRow('State', 'state', this._handleChangeState);
'State',
'state',
this._handleChangeState
);
const scope = this.renderInputRow( const scope = this.renderInputRow('Scope', 'scope', this._handleChangeScope);
'Scope',
'scope',
this._handleChangeScope
);
const username = this.renderInputRow( const username = this.renderInputRow('Username', 'username', this._handleChangeUsername);
'Username',
'username',
this._handleChangeUsername
);
const password = this.renderInputRow( const password = this.renderInputRow('Password', 'password', this._handleChangePassword);
'Password',
'password',
this._handleChangePassword
);
const tokenPrefix = this.renderInputRow( const tokenPrefix = this.renderInputRow(
'Header Prefix', 'Header Prefix',
@ -428,6 +387,13 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
'Indicate what resource server to access' 'Indicate what resource server to access'
); );
const resource = this.renderInputRow(
'Resource',
'resource',
this._handleChangeResource,
'Indicate what resource to access'
);
const credentialsInBody = this.renderSelectRow( const credentialsInBody = this.renderSelectRow(
'Credentials', 'Credentials',
'credentialsInBody', 'credentialsInBody',
@ -455,16 +421,9 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
} else if (grantType === GRANT_TYPE_CLIENT_CREDENTIALS) { } else if (grantType === GRANT_TYPE_CLIENT_CREDENTIALS) {
basicFields = [accessTokenUrl, clientId, clientSecret, enabled]; basicFields = [accessTokenUrl, clientId, clientSecret, enabled];
advancedFields = [scope, credentialsInBody, tokenPrefix, audience]; advancedFields = [scope, credentialsInBody, tokenPrefix, audience, resource];
} else if (grantType === GRANT_TYPE_PASSWORD) { } else if (grantType === GRANT_TYPE_PASSWORD) {
basicFields = [ basicFields = [username, password, accessTokenUrl, clientId, clientSecret, enabled];
username,
password,
accessTokenUrl,
clientId,
clientSecret,
enabled
];
advancedFields = [scope, credentialsInBody, tokenPrefix]; advancedFields = [scope, credentialsInBody, tokenPrefix];
} else if (grantType === GRANT_TYPE_IMPLICIT) { } else if (grantType === GRANT_TYPE_IMPLICIT) {
@ -476,9 +435,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
return { basic: basicFields, advanced: advancedFields }; return { basic: basicFields, advanced: advancedFields };
} }
static renderExpireAt( static renderExpireAt(token: OAuth2Token | null): React.Element<*> | string | null {
token: OAuth2Token | null
): React.Element<*> | string | null {
if (!token || !token.accessToken) { if (!token || !token.accessToken) {
return null; return null;
} }
@ -509,10 +466,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
const errorUriButton = const errorUriButton =
oAuth2Token && oAuth2Token.errorUri ? ( oAuth2Token && oAuth2Token.errorUri ? (
<Link <Link href={oAuth2Token.errorUri} title={oAuth2Token.errorUri} className="space-left icon">
href={oAuth2Token.errorUri}
title={oAuth2Token.errorUri}
className="space-left icon">
<i className="fa fa-question-circle" /> <i className="fa fa-question-circle" />
</Link> </Link>
) : null; ) : null;
@ -584,9 +538,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
</tbody> </tbody>
</table> </table>
<div className="notice subtle margin-top text-left"> <div className="notice subtle margin-top text-left">
{error && ( {error && <p className="selectable notice warning margin-bottom">{error}</p>}
<p className="selectable notice warning margin-bottom">{error}</p>
)}
{this.renderError()} {this.renderError()}
<div className="form-control form-control--outlined"> <div className="form-control form-control--outlined">
<label> <label>
@ -610,9 +562,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
</div> </div>
<div className="pad-top text-right"> <div className="pad-top text-right">
{tok ? ( {tok ? (
<PromptButton <PromptButton className="btn btn--clicky" onClick={this._handleClearTokens}>
className="btn btn--clicky"
onClick={this._handleClearTokens}>
Clear Clear
</PromptButton> </PromptButton>
) : null} ) : null}