diff --git a/README.md b/README.md index 1c283f44..569ac275 100644 --- a/README.md +++ b/README.md @@ -619,7 +619,7 @@ AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() ID Token validation was introduced in `0.8.0` but not all authorization servers or configurations support it correctly. -- For testing environments [setSkipIssuerHttpsCheck](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/AppAuthConfiguration.java#L129) can be used to bypass the fact the issuer needs to be HTTPS. +- For testing environments [setSkipIssuerHttpsCheck](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/AppAuthConfiguration.java#L143) can be used to bypass the fact the issuer needs to be HTTPS. ```java AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() @@ -635,6 +635,22 @@ AuthorizationRequest authRequest = authRequestBuilder .build(); ``` +- To change the default allowed time skew of 10 minutes for the issue time, [setAllowedIssueTimeSkew](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/AppAuthConfiguration.java#L159) can be used. + +```java +AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() + .setAllowedIssueTimeSkew(THIRTY_MINUTES_IN_SECONDS) + .build() +``` + +- For testing environments [setSkipIssueTimeValidation](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/AppAuthConfiguration.java#L151) can be used to bypass the issue time validation. + +```java +AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() + .setSkipIssueTimeValidation(true) + .build() +``` + ## Dynamic client registration AppAuth supports the diff --git a/library/java/net/openid/appauth/AppAuthConfiguration.java b/library/java/net/openid/appauth/AppAuthConfiguration.java index 313541df..5e6f8946 100644 --- a/library/java/net/openid/appauth/AppAuthConfiguration.java +++ b/library/java/net/openid/appauth/AppAuthConfiguration.java @@ -42,13 +42,21 @@ public class AppAuthConfiguration { private final boolean mSkipIssuerHttpsCheck; + private final boolean mSkipIssueTimeValidation; + + private final Long mAllowedIssueTimeSkew; + private AppAuthConfiguration( @NonNull BrowserMatcher browserMatcher, @NonNull ConnectionBuilder connectionBuilder, - Boolean skipIssuerHttpsCheck) { + Boolean skipIssuerHttpsCheck, + Boolean skipIssueTimeValidation, + Long allowedIssueTimeSkew) { mBrowserMatcher = browserMatcher; mConnectionBuilder = connectionBuilder; mSkipIssuerHttpsCheck = skipIssuerHttpsCheck; + mSkipIssueTimeValidation = skipIssueTimeValidation; + mAllowedIssueTimeSkew = allowedIssueTimeSkew; } /** @@ -76,6 +84,22 @@ public ConnectionBuilder getConnectionBuilder() { */ public boolean getSkipIssuerHttpsCheck() { return mSkipIssuerHttpsCheck; } + /** + * Returns true if the ID token issue time validation is disables, + * otherwise false. + * + * @see Builder#setSkipIssueTimeValidation(Boolean) + */ + public boolean getSkipIssueTimeValidation() { return mSkipIssueTimeValidation; } + + /** + * Returns the time in seconds that the ID token issue time is allowed to be + * skewed. + * + * @see Builder#setAllowedIssueTimeSkew(Long) + */ + public Long getAllowedIssueTimeSkew() { return mAllowedIssueTimeSkew; } + /** * Creates {@link AppAuthConfiguration} instances. */ @@ -84,6 +108,8 @@ public static class Builder { private BrowserMatcher mBrowserMatcher = AnyBrowserMatcher.INSTANCE; private ConnectionBuilder mConnectionBuilder = DefaultConnectionBuilder.INSTANCE; private boolean mSkipIssuerHttpsCheck; + private boolean mSkipIssueTimeValidation; + private Long mAllowedIssueTimeSkew; private boolean mSkipNonceVerification; /** @@ -119,6 +145,22 @@ public Builder setSkipIssuerHttpsCheck(Boolean skipIssuerHttpsCheck) { return this; } + /** + * Disables issue time validation for the id token. + */ + public Builder setSkipIssueTimeValidation(Boolean skipIssueTimeValidation) { + mSkipIssueTimeValidation = skipIssueTimeValidation; + return this; + } + + /** + * Sets the allowed time skew in seconds for id token issue time validation. + */ + public Builder setAllowedIssueTimeSkew(Long allowedIssueTimeSkew) { + mAllowedIssueTimeSkew = allowedIssueTimeSkew; + return this; + } + /** * Creates the instance from the configured properties. */ @@ -127,7 +169,9 @@ public AppAuthConfiguration build() { return new AppAuthConfiguration( mBrowserMatcher, mConnectionBuilder, - mSkipIssuerHttpsCheck + mSkipIssuerHttpsCheck, + mSkipIssueTimeValidation, + mAllowedIssueTimeSkew ); } diff --git a/library/java/net/openid/appauth/AuthorizationService.java b/library/java/net/openid/appauth/AuthorizationService.java index b3bdf7ed..56f59f0e 100644 --- a/library/java/net/openid/appauth/AuthorizationService.java +++ b/library/java/net/openid/appauth/AuthorizationService.java @@ -506,7 +506,9 @@ public void performTokenRequest( mClientConfiguration.getConnectionBuilder(), SystemClock.INSTANCE, callback, - mClientConfiguration.getSkipIssuerHttpsCheck()) + mClientConfiguration.getSkipIssuerHttpsCheck(), + mClientConfiguration.getSkipIssueTimeValidation(), + mClientConfiguration.getAllowedIssueTimeSkew()) .execute(); } @@ -585,6 +587,8 @@ private static class TokenRequestTask private TokenResponseCallback mCallback; private Clock mClock; private boolean mSkipIssuerHttpsCheck; + private boolean mSkipIssueTimeValidation; + private Long mAllowedIssueTimeSkew; private AuthorizationException mException; @@ -593,13 +597,17 @@ private static class TokenRequestTask @NonNull ConnectionBuilder connectionBuilder, Clock clock, TokenResponseCallback callback, - Boolean skipIssuerHttpsCheck) { + Boolean skipIssuerHttpsCheck, + Boolean skipissueTimeValidation, + Long allowedIssueTimeSkew) { mRequest = request; mClientAuthentication = clientAuthentication; mConnectionBuilder = connectionBuilder; mClock = clock; mCallback = callback; mSkipIssuerHttpsCheck = skipIssuerHttpsCheck; + mSkipIssueTimeValidation = skipissueTimeValidation; + mAllowedIssueTimeSkew = allowedIssueTimeSkew; } @Override @@ -710,7 +718,9 @@ protected void onPostExecute(JSONObject json) { idToken.validate( mRequest, mClock, - mSkipIssuerHttpsCheck + mSkipIssuerHttpsCheck, + mSkipIssueTimeValidation, + mAllowedIssueTimeSkew ); } catch (AuthorizationException ex) { mCallback.onTokenRequestCompleted(null, ex); diff --git a/library/java/net/openid/appauth/IdToken.java b/library/java/net/openid/appauth/IdToken.java index 4a4556ef..96817170 100644 --- a/library/java/net/openid/appauth/IdToken.java +++ b/library/java/net/openid/appauth/IdToken.java @@ -204,12 +204,14 @@ static IdToken from(String token) throws JSONException, IdTokenException { @VisibleForTesting void validate(@NonNull TokenRequest tokenRequest, Clock clock) throws AuthorizationException { - validate(tokenRequest, clock, false); + validate(tokenRequest, clock, false, false, null); } void validate(@NonNull TokenRequest tokenRequest, Clock clock, - boolean skipIssuerHttpsCheck) throws AuthorizationException { + boolean skipIssuerHttpsCheck, + boolean skipIssueTimeValidation, + @Nullable Long allowedIssueTimeSkew) throws AuthorizationException { // OpenID Connect Core Section 3.1.3.7. rule #1 // Not enforced: AppAuth does not support JWT encryption. @@ -276,13 +278,16 @@ void validate(@NonNull TokenRequest tokenRequest, new IdTokenException("ID Token expired")); } - // OpenID Connect Core Section 3.1.3.7. rule #10 - // Validates that the issued at time is not more than +/- 10 minutes on the current - // time. - if (Math.abs(nowInSeconds - this.issuedAt) > TEN_MINUTES_IN_SECONDS) { - throw AuthorizationException.fromTemplate(GeneralErrors.ID_TOKEN_VALIDATION_ERROR, - new IdTokenException("Issued at time is more than 10 minutes " - + "before or after the current time")); + + if (!skipIssueTimeValidation) { + // OpenID Connect Core Section 3.1.3.7. rule #10 + // Validates that the issued at time is not more than the +/- configured allowed time skew, + // or +/- 10 minutes as a default, on the current time. + if (Math.abs(nowInSeconds - this.issuedAt) > (allowedIssueTimeSkew == null ? TEN_MINUTES_IN_SECONDS : allowedIssueTimeSkew)) { + throw AuthorizationException.fromTemplate(GeneralErrors.ID_TOKEN_VALIDATION_ERROR, + new IdTokenException("Issued at time is more than 10 minutes " + + "before or after the current time")); + } } // Only relevant for the authorization_code response type diff --git a/library/javatests/net/openid/appauth/IdTokenTest.java b/library/javatests/net/openid/appauth/IdTokenTest.java index 13944f7c..7b5a679e 100644 --- a/library/javatests/net/openid/appauth/IdTokenTest.java +++ b/library/javatests/net/openid/appauth/IdTokenTest.java @@ -272,7 +272,7 @@ public void testValidate_shouldSkipNonHttpsIssuer() .setRedirectUri(TEST_APP_REDIRECT_URI) .build(); Clock clock = SystemClock.INSTANCE; - idToken.validate(tokenRequest, clock, true); + idToken.validate(tokenRequest, clock, true, false, null); } @Test(expected = AuthorizationException.class) @@ -464,6 +464,60 @@ public void testValidate_shouldFailOnIssuedAtOverTenMinutesAgo() throws Authoriz idToken.validate(tokenRequest, clock); } + @Test + public void testValidate_withSkipIssueTimeValidation() throws AuthorizationException { + Long nowInSeconds = SystemClock.INSTANCE.getCurrentTimeMillis() / 1000; + Long anHourInSeconds = (long) (60 * 60); + IdToken idToken = new IdToken( + TEST_ISSUER, + TEST_SUBJECT, + Collections.singletonList(TEST_CLIENT_ID), + nowInSeconds, + nowInSeconds - (anHourInSeconds * 2), + TEST_NONCE, + TEST_CLIENT_ID + ); + TokenRequest tokenRequest = getAuthCodeExchangeRequestWithNonce(); + Clock clock = SystemClock.INSTANCE; + idToken.validate(tokenRequest, clock, false, true, null); + } + + @Test(expected = AuthorizationException.class) + public void testValidate_shouldFailOnIssuedAtOverConfiguredTimeSkew() throws AuthorizationException { + Long nowInSeconds = SystemClock.INSTANCE.getCurrentTimeMillis() / 1000; + Long anHourInSeconds = (long) (60 * 60); + IdToken idToken = new IdToken( + TEST_ISSUER, + TEST_SUBJECT, + Collections.singletonList(TEST_CLIENT_ID), + nowInSeconds, + nowInSeconds - anHourInSeconds - 1, + TEST_NONCE, + TEST_CLIENT_ID + ); + TokenRequest tokenRequest = getAuthCodeExchangeRequestWithNonce(); + Clock clock = SystemClock.INSTANCE; + idToken.validate(tokenRequest, clock, false, false, anHourInSeconds); + } + + @Test + public void testValidate_withConfiguredTimeSkew() throws AuthorizationException { + Long nowInSeconds = SystemClock.INSTANCE.getCurrentTimeMillis() / 1000; + Long anHourInSeconds = (long) (60 * 60); + IdToken idToken = new IdToken( + TEST_ISSUER, + TEST_SUBJECT, + Collections.singletonList(TEST_CLIENT_ID), + nowInSeconds, + nowInSeconds - anHourInSeconds, + TEST_NONCE, + TEST_CLIENT_ID + ); + TokenRequest tokenRequest = getAuthCodeExchangeRequestWithNonce(); + Clock clock = SystemClock.INSTANCE; + idToken.validate(tokenRequest, clock, false, false, anHourInSeconds); + } + @Test(expected = AuthorizationException.class) public void testValidate_shouldFailOnNonceMismatch() throws AuthorizationException { Long nowInSeconds = SystemClock.INSTANCE.getCurrentTimeMillis() / 1000;