Skip to content

Custom time skew for IdToken validation #1124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
47 changes: 44 additions & 3 deletions library/java/net/openid/appauth/AppAuthConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -76,6 +83,22 @@ public ConnectionBuilder getConnectionBuilder() {
*/
public boolean getSkipIssuerHttpsCheck() { return mSkipIssuerHttpsCheck; }

/**
* Returns <code>true</code> if the ID token issue time validation is disables,
* otherwise <code>false</code>.
*
* @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.
*/
Expand All @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand All @@ -127,7 +166,9 @@ public AppAuthConfiguration build() {
return new AppAuthConfiguration(
mBrowserMatcher,
mConnectionBuilder,
mSkipIssuerHttpsCheck
mSkipIssuerHttpsCheck,
mSkipTimeValidation,
mAllowedTimeSkew
);
}

Expand Down
17 changes: 13 additions & 4 deletions library/java/net/openid/appauth/AuthorizationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,9 @@ public void performTokenRequest(
mClientConfiguration.getConnectionBuilder(),
SystemClock.INSTANCE,
callback,
mClientConfiguration.getSkipIssuerHttpsCheck())
mClientConfiguration.getSkipIssuerHttpsCheck(),
mClientConfiguration.getSkipTimeValidation(),
mClientConfiguration.getAllowedTimeSkew())
.execute();
}

Expand Down Expand Up @@ -585,21 +587,26 @@ 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,
@NonNull ClientAuthentication clientAuthentication,
@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
Expand Down Expand Up @@ -710,7 +717,9 @@ protected void onPostExecute(JSONObject json) {
idToken.validate(
mRequest,
mClock,
mSkipIssuerHttpsCheck
mSkipIssuerHttpsCheck,
mSkipTimeValidation,
mAllowedTimeSkew
);
} catch (AuthorizationException ex) {
mCallback.onTokenRequestCompleted(null, ex);
Expand Down
31 changes: 19 additions & 12 deletions library/java/net/openid/appauth/IdToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
56 changes: 55 additions & 1 deletion library/javatests/net/openid/appauth/IdTokenTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down