diff --git a/README.md b/README.md index 1c283f44..d4e61978 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ this redirect URI. We recommend using a custom scheme based redirect URI (i.e. those of form `my.scheme:/path`), as this is the most widely supported across all versions of -Android. To avoid conflicts with other apps, it is recommended to configure a +Android. To avoid conflicts with other apps, it is recommended to configure a distinct scheme using "reverse domain name notation". This can either match your service web domain (in reverse) e.g. `com.example.service` or your package name `com.example.app` or be something completely new as long as it's distinct @@ -619,12 +619,12 @@ 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#L141) can be used to bypass the fact the issuer needs to be HTTPS. ```java AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() .setSkipIssuerHttpsCheck(true) - .build() + .build(); ``` - For services that don't support nonce[s] resulting in **IdTokenException** `Nonce mismatch` just set nonce to `null` on the `AuthorizationRequest`. Please consider **raising an issue** with your Identity Provider and removing this once it is fixed. @@ -635,6 +635,23 @@ AuthorizationRequest authRequest = authRequestBuilder .build(); ``` +- For testing environments [setSkipTimeValidation](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/AppAuthConfiguration.java#L149) can be used to bypass the issue time validation. + +```java +AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() + .setSkipTimeValidation(true) + .build(); +``` + +- To change the default allowed time skew of 10 minutes for the issue time, [setAllowedTimeSkew](https://github.com/openid/AppAuth-Android/blob/master/library/java/net/openid/appauth/AppAuthConfiguration.java#L157) can be used. + +```java +AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() + .setAllowedTimeSkew(TWENTY_MINUTES_IN_SECONDS) + .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..696ebeb5 100644 --- a/library/java/net/openid/appauth/AppAuthConfiguration.java +++ b/library/java/net/openid/appauth/AppAuthConfiguration.java @@ -42,13 +42,20 @@ public class AppAuthConfiguration { private final boolean mSkipIssuerHttpsCheck; + private final boolean mSkipTimeValidation; + + private final Long mAllowedTimeSkew; private AppAuthConfiguration( @NonNull BrowserMatcher browserMatcher, @NonNull ConnectionBuilder connectionBuilder, - Boolean skipIssuerHttpsCheck) { + Boolean skipIssuerHttpsCheck, + Boolean skipTimeValidation, + Long allowedTimeSkew) { mBrowserMatcher = browserMatcher; mConnectionBuilder = connectionBuilder; mSkipIssuerHttpsCheck = skipIssuerHttpsCheck; + mSkipTimeValidation = skipTimeValidation; + mAllowedTimeSkew = allowedTimeSkew; } /** @@ -76,6 +83,22 @@ public ConnectionBuilder getConnectionBuilder() { */ public boolean getSkipIssuerHttpsCheck() { return mSkipIssuerHttpsCheck; } + /** + * Returns true if the ID token issue time validation is disables, + * otherwise false. + * + * @see Builder#setSkipTimeValidation(Boolean) + */ + public boolean getSkipTimeValidation() { return mSkipTimeValidation; } + + /** + * Returns the time in seconds that the ID token issue time is allowed to be + * skewed. + * + * @see Builder#setAllowedTimeSkew(Long) + */ + public Long getAllowedTimeSkew() { return mAllowedTimeSkew; } + /** * Creates {@link AppAuthConfiguration} instances. */ @@ -85,7 +108,8 @@ public static class Builder { private ConnectionBuilder mConnectionBuilder = DefaultConnectionBuilder.INSTANCE; private boolean mSkipIssuerHttpsCheck; private boolean mSkipNonceVerification; - + private boolean mSkipTimeValidation; + private Long mAllowedTimeSkew; /** * Specify the browser matcher to use, which controls the browsers that can be used * for authorization. @@ -119,6 +143,21 @@ public Builder setSkipIssuerHttpsCheck(Boolean skipIssuerHttpsCheck) { return this; } + /** + * Disables issue time validation for the id token. + */ + public Builder setSkipTimeValidation(Boolean skipTimeValidation) { + mSkipTimeValidation = skipTimeValidation; + return this; + } + + /** + * Sets the allowed time skew in seconds for id token issue time validation. + */ + public Builder setAllowedTimeSkew(Long allowedTimeSkew) { + mAllowedTimeSkew = allowedTimeSkew; + return this; + } /** * Creates the instance from the configured properties. */ @@ -127,7 +166,9 @@ public AppAuthConfiguration build() { return new AppAuthConfiguration( mBrowserMatcher, mConnectionBuilder, - mSkipIssuerHttpsCheck + mSkipIssuerHttpsCheck, + mSkipTimeValidation, + mAllowedTimeSkew ); } diff --git a/library/java/net/openid/appauth/AuthorizationService.java b/library/java/net/openid/appauth/AuthorizationService.java index b3bdf7ed..93467303 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.getSkipTimeValidation(), + mClientConfiguration.getAllowedTimeSkew()) .execute(); } @@ -585,7 +587,8 @@ private static class TokenRequestTask private TokenResponseCallback mCallback; private Clock mClock; private boolean mSkipIssuerHttpsCheck; - + private boolean mSkipTimeValidation; + private Long mAllowedTimeSkew; private AuthorizationException mException; TokenRequestTask(TokenRequest request, @@ -593,13 +596,17 @@ private static class TokenRequestTask @NonNull ConnectionBuilder connectionBuilder, Clock clock, TokenResponseCallback callback, - Boolean skipIssuerHttpsCheck) { + Boolean skipIssuerHttpsCheck, + Boolean skipTimeValidation, + Long allowedTimeSkew) { mRequest = request; mClientAuthentication = clientAuthentication; mConnectionBuilder = connectionBuilder; mClock = clock; mCallback = callback; mSkipIssuerHttpsCheck = skipIssuerHttpsCheck; + mSkipTimeValidation = skipTimeValidation; + mAllowedTimeSkew = allowedTimeSkew; } @Override @@ -710,7 +717,9 @@ protected void onPostExecute(JSONObject json) { idToken.validate( mRequest, mClock, - mSkipIssuerHttpsCheck + mSkipIssuerHttpsCheck, + mSkipTimeValidation, + mAllowedTimeSkew ); } 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..cd3be9c6 100644 --- a/library/java/net/openid/appauth/IdToken.java +++ b/library/java/net/openid/appauth/IdToken.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Base64; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -204,12 +205,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 skipTimeValidation, + @Nullable Long allowedTimeSkew) throws AuthorizationException { // OpenID Connect Core Section 3.1.3.7. rule #1 // Not enforced: AppAuth does not support JWT encryption. @@ -271,18 +274,22 @@ void validate(@NonNull TokenRequest tokenRequest, // OpenID Connect Core Section 3.1.3.7. rule #9 // Validates that the current time is before the expiry time. Long nowInSeconds = clock.getCurrentTimeMillis() / MILLIS_PER_SECOND; - if (nowInSeconds > this.expiration) { - throw AuthorizationException.fromTemplate(GeneralErrors.ID_TOKEN_VALIDATION_ERROR, - new IdTokenException("ID Token expired")); + if (!skipTimeValidation) { + if (nowInSeconds > this.expiration) { + throw AuthorizationException.fromTemplate(GeneralErrors.ID_TOKEN_VALIDATION_ERROR, + 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 (!skipTimeValidation) { + // 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) > (allowedTimeSkew == null ? TEN_MINUTES_IN_SECONDS : allowedTimeSkew)) { + 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..5799f94a 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;