Implementing authorization code grant OpenID/OAuth with AppAuth-JS 1.3.1 may require these workarounds:
- The public API for
RedirectRequestHandlerdoes not expose an auth flow completion method that gives negative feedback; if the callback does not contain proper authorization results (token, code etc.) the currentcompleteAuthorizationRequestIfPossible()method fails silently with a console log, not giving any indication to the caller. To overcome this, you can subclassRedirectRequestHandlerto change (expose) the already existing internalcompleteAuthorizationRequest()method fromprotectedtopublic:import {RedirectRequestHandler} from '@openid/appauth'; // .. class CallableRedirectRequestHandler extends RedirectRequestHandler { completeAuthorizationRequest() { return super.completeAuthorizationRequest(); } }Afterwards you can modify the auth completion path to use aCallableRedirectRequestHandlerinstance, and directly callcompleteAuthorizationRequest(), with a null-check on the result included in the returnedPromiseto handle missing/failed auth cases.
- Callback URLs from code grants usually contain result/return data (
code,stateetc.) as query params, unlike implicit grant which typically uses a hash (fortoken,stateetc.). However the defaultBasicQueryStringUtilsalways parses the callback URL assuming a hash format. When code grant is in use, one possible workaround is to extendparse()method ofBasicQueryStringUtils, to always override/turn offuseHashwhen it is invoked internally:import {BasicQueryStringUtils} from '@openid/appauth'; // .. class NoHashQueryStringUtils extends BasicQueryStringUtils { parse(input, useHash?) { return super.parse(input, false); } }
With these changes, an authorization code grant flow with AppAuth-JS in Angular may look like this:
import {
AuthorizationRequest,
AuthorizationServiceConfiguration,
RedirectRequestHandler,
BasicQueryStringUtils
} from '@openid/appauth';
// your OpenID provider configs
const config = {
baseUrl: "your OpenID provider's base URL",
clientId: "client ID",
scopes: "space separated scopes list",
tokenName: "key of the token field returned in the back-end code-exchange response; in case it is customized/changeable"
};
// ... component code
/* call this method to start the auth process
AppAuth will store request details into localStorage, and recover it from there during the next/completion phase */
async initiateAuth() {
let serverConfig;
try {
serverConfig = await AuthorizationServiceConfiguration.fetchFromIssuer(config.baseUrl);
} catch (e) {
throw new Error(`Error loading auth server details: ${e.message}`);
}
const handler = new RedirectRequestHandler();
const request = new AuthorizationRequest({
client_id: config.clientId,
redirect_uri: `${window.location.origin}/openid-callback`, // your callback URL
scope: config.scopes || 'openid',
response_type: AuthorizationRequest.RESPONSE_TYPE_CODE
});
handler.performAuthorizationRequest(serverConfig, request);
}
/* call this method when callback URL/page has loaded
if previously saved details could be loaded from localStorage, and return parameters could be extracted successfully
from the (callback) URL, completion call will receive an auth result;
otherwise it will receive an empty/undefined value, in which case you can notify the user that auth was unsuccessful */
async completeAuth() {
const authResult = await new CallableRedirectRequestHandler(undefined, new NoHashQueryStringUtils())
.completeAuthorizationRequest();
if (!authResult) {
throw new Error('Could not find an authentication attempt to proceed; did you open this page directly, without going through log-in?');
}
let contentHeader: Headers = new Headers();
contentHeader.append('Content-Type', 'application/json');
return this.http.post(/* your code-exchange backend URL */, JSON.stringify({
code: authResult.response.code,
verifier: authResult.request.internal['code_verifier']
}), {
headers: contentHeader
}).toPromise()
.then(response => response.json()[config.tokenName || 'id_token'])
.catch(e => /* handle it */);
}
// ..