Skip to content

Commit

Permalink
Implement ID Token validation according to OIDC spec (#385)
Browse files Browse the repository at this point in the history
Validate ID Tokens (if any) on TokenResponses according to http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
  • Loading branch information
oyvindrobertsen authored and iainmcgin committed Aug 10, 2018
1 parent 5a58643 commit 1a47f74
Show file tree
Hide file tree
Showing 12 changed files with 1,001 additions and 56 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -15,8 +15,8 @@ buildscript {

subprojects {
repositories {
jcenter()
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
Expand Down
12 changes: 12 additions & 0 deletions library/java/net/openid/appauth/AuthorizationException.java
Expand Up @@ -200,6 +200,18 @@ public static final class GeneralErrors {
*/
public static final AuthorizationException INVALID_REGISTRATION_RESPONSE =
generalEx(7, "Invalid registration response");

/**
* Indicates that a received ID token could not be parsed
*/
public static final AuthorizationException ID_TOKEN_PARSING_ERROR =
generalEx(8, "Unable to parse ID Token");

/**
* Indicates that a received ID token is invalid
*/
public static final AuthorizationException ID_TOKEN_VALIDATION_ERROR =
generalEx(9, "Invalid ID Token");
}

/**
Expand Down
43 changes: 43 additions & 0 deletions library/java/net/openid/appauth/AuthorizationRequest.java
Expand Up @@ -302,6 +302,9 @@ public static final class ResponseMode {
@VisibleForTesting
static final String PARAM_STATE = "state";

@VisibleForTesting
static final String PARAM_NONCE = "nonce";

private static final Set<String> BUILT_IN_PARAMS = builtInParams(
PARAM_CLIENT_ID,
PARAM_CODE_CHALLENGE,
Expand All @@ -324,6 +327,7 @@ public static final class ResponseMode {
private static final String KEY_REDIRECT_URI = "redirectUri";
private static final String KEY_SCOPE = "scope";
private static final String KEY_STATE = "state";
private static final String KEY_NONCE = "nonce";
private static final String KEY_CODE_VERIFIER = "codeVerifier";
private static final String KEY_CODE_VERIFIER_CHALLENGE = "codeVerifierChallenge";
private static final String KEY_CODE_VERIFIER_CHALLENGE_METHOD = "codeVerifierChallengeMethod";
Expand Down Expand Up @@ -436,6 +440,19 @@ public static final class ResponseMode {
@Nullable
public final String state;

/**
* String value used to associate a Client session with an ID Token, and to mitigate replay
* attacks. The value is passed through unmodified from the Authentication Request to the ID
* Token. If this value is not explicitly set, this library will automatically add nonce and
* perform appropriate validation of the ID Token. It is recommended that the default
* implementation of this parameter be used wherever possible.
*
* @see "OpenID Connect Core 1.0, Section 3.1.2.1
* <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.1>"
*/
@Nullable
public final String nonce;

/**
* The proof key for code exchange. This is an opaque value used to associate an authorization
* request with a subsequent code exchange, in order to prevent any eavesdropping party from
Expand Down Expand Up @@ -542,6 +559,9 @@ public static final class Builder {
@Nullable
private String mState;

@Nullable
private String mNonce;

@Nullable
private String mCodeVerifier;

Expand Down Expand Up @@ -570,6 +590,7 @@ public Builder(
setResponseType(responseType);
setRedirectUri(redirectUri);
setState(AuthorizationRequest.generateRandomState());
setNonce(AuthorizationRequest.generateRandomState());
setCodeVerifier(CodeVerifierUtil.generateRandomCodeVerifier());
}

Expand Down Expand Up @@ -763,6 +784,22 @@ public Builder setState(@Nullable String state) {
return this;
}

/**
* Specifies the String value used to associate a Client session with an ID Token, and to
* mitigate replay attacks. The value is passed through unmodified from the Authentication
* Request to the ID Token. If this value is not explicitly set, this library will
* automatically add nonce and perform appropriate validation of the ID Token. It is
* recommended that the default implementation of this parameter be used wherever possible.
*
* @see "OpenID Connect Core 1.0, Section 3.1.2.1
* <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.1>"
*/
@NonNull
public Builder setNonce(@Nullable String nonce) {
mNonce = checkNullOrNotEmpty(nonce, "state cannot be empty if defined");
return this;
}

/**
* Specifies the code verifier to use for this authorization request. The default challenge
* method (typically {@link #CODE_CHALLENGE_METHOD_S256}) implemented by
Expand Down Expand Up @@ -877,6 +914,7 @@ public AuthorizationRequest build() {
mPrompt,
mScope,
mState,
mNonce,
mCodeVerifier,
mCodeVerifierChallenge,
mCodeVerifierChallengeMethod,
Expand All @@ -895,6 +933,7 @@ private AuthorizationRequest(
@Nullable String prompt,
@Nullable String scope,
@Nullable String state,
@Nullable String nonce,
@Nullable String codeVerifier,
@Nullable String codeVerifierChallenge,
@Nullable String codeVerifierChallengeMethod,
Expand All @@ -913,6 +952,7 @@ private AuthorizationRequest(
this.prompt = prompt;
this.scope = scope;
this.state = state;
this.nonce = nonce;
this.codeVerifier = codeVerifier;
this.codeVerifierChallenge = codeVerifierChallenge;
this.codeVerifierChallengeMethod = codeVerifierChallengeMethod;
Expand Down Expand Up @@ -952,6 +992,7 @@ public Uri toUri() {
UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_LOGIN_HINT, loginHint);
UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_PROMPT, prompt);
UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_STATE, state);
UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_NONCE, nonce);
UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_SCOPE, scope);
UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_RESPONSE_MODE, responseMode);

Expand Down Expand Up @@ -983,6 +1024,7 @@ public JSONObject jsonSerialize() {
JsonUtil.putIfNotNull(json, KEY_SCOPE, scope);
JsonUtil.putIfNotNull(json, KEY_PROMPT, prompt);
JsonUtil.putIfNotNull(json, KEY_STATE, state);
JsonUtil.putIfNotNull(json, KEY_NONCE, nonce);
JsonUtil.putIfNotNull(json, KEY_CODE_VERIFIER, codeVerifier);
JsonUtil.putIfNotNull(json, KEY_CODE_VERIFIER_CHALLENGE, codeVerifierChallenge);
JsonUtil.putIfNotNull(json, KEY_CODE_VERIFIER_CHALLENGE_METHOD,
Expand Down Expand Up @@ -1020,6 +1062,7 @@ public static AuthorizationRequest jsonDeserialize(@NonNull JSONObject json)
.setLoginHint(JsonUtil.getStringIfDefined(json, KEY_LOGIN_HINT))
.setPrompt(JsonUtil.getStringIfDefined(json, KEY_PROMPT))
.setState(JsonUtil.getStringIfDefined(json, KEY_STATE))
.setNonce(JsonUtil.getStringIfDefined(json, KEY_NONCE))
.setCodeVerifier(
JsonUtil.getStringIfDefined(json, KEY_CODE_VERIFIER),
JsonUtil.getStringIfDefined(json, KEY_CODE_VERIFIER_CHALLENGE),
Expand Down
1 change: 1 addition & 0 deletions library/java/net/openid/appauth/AuthorizationResponse.java
Expand Up @@ -466,6 +466,7 @@ public TokenRequest createTokenExchangeRequest(
.setCodeVerifier(request.codeVerifier)
.setAuthorizationCode(authorizationCode)
.setAdditionalParameters(additionalExchangeParameters)
.setNonce(request.nonce)
.build();
}

Expand Down
25 changes: 25 additions & 0 deletions library/java/net/openid/appauth/AuthorizationService.java
Expand Up @@ -34,6 +34,7 @@
import net.openid.appauth.AuthorizationException.RegistrationRequestErrors;
import net.openid.appauth.AuthorizationException.TokenRequestErrors;

import net.openid.appauth.IdToken.IdTokenException;
import net.openid.appauth.browser.BrowserDescriptor;
import net.openid.appauth.browser.BrowserSelector;
import net.openid.appauth.browser.CustomTabManager;
Expand Down Expand Up @@ -323,6 +324,7 @@ public void performTokenRequest(
request,
clientAuthentication,
mClientConfiguration.getConnectionBuilder(),
SystemClock.INSTANCE,
callback)
.execute();
}
Expand Down Expand Up @@ -394,20 +396,24 @@ private Intent prepareAuthorizationRequestIntent(

private static class TokenRequestTask
extends AsyncTask<Void, Void, JSONObject> {

private TokenRequest mRequest;
private ClientAuthentication mClientAuthentication;
private final ConnectionBuilder mConnectionBuilder;
private TokenResponseCallback mCallback;
private Clock mClock;

private AuthorizationException mException;

TokenRequestTask(TokenRequest request,
@NonNull ClientAuthentication clientAuthentication,
@NonNull ConnectionBuilder connectionBuilder,
Clock clock,
TokenResponseCallback callback) {
mRequest = request;
mClientAuthentication = clientAuthentication;
mConnectionBuilder = connectionBuilder;
mClock = clock;
mCallback = callback;
}

Expand Down Expand Up @@ -503,6 +509,25 @@ protected void onPostExecute(JSONObject json) {
return;
}

if (response.idToken != null) {
IdToken idToken;
try {
idToken = IdToken.from(response.idToken);
} catch (IdTokenException | JSONException ex) {
mCallback.onTokenRequestCompleted(null,
AuthorizationException.fromTemplate(
GeneralErrors.ID_TOKEN_PARSING_ERROR,
ex));
return;
}

try {
idToken.validate(mRequest, mClock);
} catch (AuthorizationException ex) {
mCallback.onTokenRequestCompleted(null, ex);
return;
}
}
Logger.debug("Token exchange with %s completed",
mRequest.configuration.tokenEndpoint);
mCallback.onTokenRequestCompleted(response, null);
Expand Down

0 comments on commit 1a47f74

Please sign in to comment.