Skip to content

ID Token validation should support clock skew #5839

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

Closed
zcwang3 opened this issue Sep 13, 2018 · 19 comments
Closed

ID Token validation should support clock skew #5839

zcwang3 opened this issue Sep 13, 2018 · 19 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement
Milestone

Comments

@zcwang3
Copy link

zcwang3 commented Sep 13, 2018

Summary

I am using spring security 5.0.7 with spring boot 2.0.4.RELEASE for login with Google and Login with Microsoft feature.

I meet the same issue of OidcAuthorizationCodeAuthenticationProvider maxIssuedAt which is closed without any enhancement

Actual Behavior

Today, when i login with Google account in one of my testing machine, I meet the issue with follow error message:
c.c.w.g.c.SecurityConfiguration$2 - Authentication request failed: org.springframework.security.oauth2.core.OAuth2AuthenticationException: [invalid_id_token]

it does not output the detail reason why id token is invalid: like aud is null or audience is not same with client id or idToken is expired etc.

After debug, I find that the failure reason is because of issuedAt.isAfter(maxIssuedAt) check failure, you can see detail error reason in the following screen shots:
image2018-9-13_17-2-39

Expected Behavior

Login with Microsoft and Google account should be successful in all different machines with some grace time

Please notice
Login with Microsoft account with ID token is successful in the same machine.
Login with Google account with ID token is failure because of issuedAt.isAfter(maxIssuedAt) check failure
Login with Microsoft and Google account with ID token are both successful in other testing machines

Since we can not control the issuedAt value which generated by Microsoft or Google and issuedAt generated by Microsoft and Google is obviously different in my testing machine.

Enhance request 1: add detail error reason in exception when id_tioken validate failure
Enhance request 2: default 30 second in the check of issuedAt.isAfter(maxIssuedAt) becomes configurable
Suggestion to fix with existing spring security version : And for current spring security version without previous enhance support, how can I do to change the value of 30 to 300s, do we have some extension point for it without change sourcecode in jar?

Configuration

no special configuration, the issue is just related with id token check logic in OidcAuthorizationCodeAuthenticationProvider

Version

org.springframework.boot spring-boot-starter-parent 2.0.4.RELEASE
 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>

Sample

no special configuration, the issue is just related with id token check logic in OidcAuthorizationCodeAuthenticationProvider

@zcwang3 zcwang3 changed the title request to enhance ID token validation in OidcAuthorizationCodeAuthenticationProvider maxIssuedAt check issue in OidcAuthorizationCodeAuthenticationProvider and id token validation enhance requirement Sep 13, 2018
@rwinch rwinch added the status: waiting-for-triage An issue we've not yet triaged label Sep 13, 2018
@jgrandja
Copy link
Contributor

@zcwang3 You've tested 3 different scenarios with 2 passing and 1 failing so this suggests a possible environment issue. I took a look at the debug screenshot you provided and it looks like you have a clock sync issue between the computer you're testing on and Google's clock.

2018-09-13T08:55:26.893Z - Now (Testing machine clock)
2018-09-13T08:56:02Z - IssuedAt (Google's clock)

As you can see, the clock on your testing machine is not in sync with Google's clock. Looks like the testing machine clock is approx. 36 secs behind Google's clock. Can you adjust the clock on the testing machine and re-test?

Please also see this comment for more info.

There is no current way for a user to configure maxIssuedAt at the moment. So to get around this issue you'll need to ensure the clocks in your application environments are in sync (or close) with the Provider environments.

We can look at adding this support in a future release. And I also feel that a more detailed exception message for ID Token validation failures is needed.

@jgrandja jgrandja added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Sep 14, 2018
@zcwang3
Copy link
Author

zcwang3 commented Sep 15, 2018

@jgrandja Thanks for you checking.

Please notice: in the same test machine, Microsoft ID token verification is pass and Google ID token verification is failure because of maxIssuedAt check issue.
it means that the clock on my testing machine is sync with Microsoft's clock, and it is not sync with Google's clock.

After I have sync clock on my testing machine with ntp command, the same issue still exists.

So i think clock out of sync issue should exist in Google and I can do nothing for that.

So, I need increate grace time of maxIssuedAt check to avoid potential production issue caused by clock out of sync in OpenID provider (like Microsoft / Google / etc...)

@jgrandja
Copy link
Contributor

@zcwang3

in the same test machine, Microsoft ID token verification is pass and Google ID token verification is failure because of maxIssuedAt check issue.
it means that the clock on my testing machine is sync with Microsoft's clock, and it is not sync with Google's clock.

Can you please provide another sceenshot from the test machine for Microsoft and Google. I'd like to see what the issueAt and now times are.
Thanks.

@zcwang3
Copy link
Author

zcwang3 commented Sep 18, 2018

Hi @jgrandja

here is the attached screen shots for Microsoft / Google id token verification

Google ID token verification: Google clock is about 40s faster than our server
google
Microsoft ID token verification: Microsoft clock is about 5 minutes slower than our server
microsoft

BTW:
as work around solution to avoid potential production issue, i copy code from OidcAuthorizationCodeAuthenticationProvider and then generate a new class MyOidcAuthorizationCodeAuthenticationProvider, in this new class i have change value from 30 to 300 and add detail error reason when token is invalid. You can refer to the following source code

private void validateIdToken(OidcIdToken idToken, ClientRegistration clientRegistration) {
// 3.1.3.7 ID Token Validation
// http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation

    // Validate REQUIRED Claims
    URL issuer = idToken.getIssuer();
    if (issuer == null) {
        this.throwInvalidIdTokenException("issuer is null");
    }
    String subject = idToken.getSubject();
    if (subject == null) {
        this.throwInvalidIdTokenException("subject is null");
    }
    List<String> audience = idToken.getAudience();
    if (CollectionUtils.isEmpty(audience)) {
        this.throwInvalidIdTokenException("audience is empty");
    }
    Instant expiresAt = idToken.getExpiresAt();
    if (expiresAt == null) {
        this.throwInvalidIdTokenException("expiresAt is null");
    }
    Instant issuedAt = idToken.getIssuedAt();
    if (issuedAt == null) {
        this.throwInvalidIdTokenException("issuedAt is null");
    }

    // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
    // MUST exactly match the value of the iss (issuer) Claim.
    // TODO Depends on gh-4413

    // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value
    // registered at the Issuer identified by the iss (issuer) Claim as an audience.
    // The aud (audience) Claim MAY contain an array with more than one element.
    // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
    // or if it contains additional audiences not trusted by the Client.
    if (!audience.contains(clientRegistration.getClientId())) {
        this.throwInvalidIdTokenException("client id does not match with audience, audience is " + audience);
    }

    // 4. If the ID Token contains multiple audiences,
    // the Client SHOULD verify that an azp Claim is present.
    String authorizedParty = idToken.getAuthorizedParty();
    if (audience.size() > 1 && authorizedParty == null) {
        this.throwInvalidIdTokenException("authorizedParty is null");
    }

    // 5. If an azp (authorized party) Claim is present,
    // the Client SHOULD verify that its client_id is the Claim Value.
    if (authorizedParty != null && !authorizedParty.equals(clientRegistration.getClientId())) {
        this.throwInvalidIdTokenException("client id does not match with authorizedParty, authorizedParty is " + authorizedParty);
    }

    // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
    // in the id_token_signed_response_alg parameter during Registration.
    // TODO Depends on gh-4413

    // 9. The current time MUST be before the time represented by the exp Claim.
    Instant now = Instant.now();
    if (!now.isBefore(expiresAt)) {
        this.throwInvalidIdTokenException(**"token is expired, now is " + now + ", expiresAt is " + expiresAt**);
    }

    // 10. The iat Claim can be used to reject tokens that were issued too far away from the current time,
    // limiting the amount of time that nonces need to be stored to prevent attacks.
    // The acceptable range is Client specific.
    Instant maxIssuedAt = Instant.now().plusSeconds(**300**);
    if (issuedAt.isAfter(maxIssuedAt)) {
        this.throwInvalidIdTokenException(**"maxIssuedAt check failure, now is " + now + ", issuedAt is " + issuedAt  + ", maxIssuedAt is " + maxIssuedAt**);
    }

    // 11. If a nonce value was sent in the Authentication Request,
    // a nonce Claim MUST be present and its value checked to verify
    // that it is the same value as the one that was sent in the Authentication Request.
    // The Client SHOULD check the nonce value for replay attacks.
    // The precise method for detecting replay attacks is Client specific.
    // TODO Depends on gh-4442

}

private void throwInvalidIdTokenException(**String errorReason**) {
    OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE);
    throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString() + ":" + errorReason);
}

@zcwang3
Copy link
Author

zcwang3 commented Sep 25, 2018

Hi @jgrandja

Do we have any conclusion/plan for this issue?

@rwinch rwinch added this to the 5.1.1 milestone Oct 3, 2018
@jgrandja
Copy link
Contributor

jgrandja commented Oct 4, 2018

@zcwang3 Apologies for the delayed response. We were quite busy leading up to the 5.1 release and SpringOne last week.

I do have a plan on how to provide a configurable clock skew value for maxIssuedAt. I'm quite busy with other priorities at the moment though.

Would you be interested in submitting a PR for this? I could guide you through the process and detail the plan/idea I have for this change.

@zcwang3
Copy link
Author

zcwang3 commented Oct 8, 2018

@jgrandja Sorry for late response because of holiday leave. I can have a try to fix this issue.
Could you please help to share the information you mentioned?
And which location is suggested for configuration of maxIssuedAt?

@jgrandja
Copy link
Contributor

jgrandja commented Oct 9, 2018

@zcwang3 There are 2 issues that will address this #5930 and #5717.

I started on #5717 but ran into a design issue so it didn't make it into 5.1.
I'll get back to this one soon.

What are your thoughts on working on #5930?

@jgrandja jgrandja modified the milestones: 5.1.1, 5.2.0 Oct 12, 2018
@zcwang3
Copy link
Author

zcwang3 commented Oct 17, 2018

@jgrandja I am work overtime for my daily work and not have time to work on #5930 now.
May be I will try to pick it up later if I have time.

Thanks

@jgrandja
Copy link
Contributor

@zcwang3 No worries. I'll try to get to it soon as well. If you get to it before me please let me know. Thanks.

@rwinch
Copy link
Member

rwinch commented Oct 17, 2018

@jgrandja In the meantime should we rename this to be something like...OidcAuthorizationCodeAuthenticationProvider should support clock skew?

@jgrandja jgrandja changed the title maxIssuedAt check issue in OidcAuthorizationCodeAuthenticationProvider and id token validation enhance requirement ID Token validation should support clock skew Oct 17, 2018
@jgrandja
Copy link
Contributor

@rwinch Yes, thanks for pointing that out. I renamed it to be more specific to ID Token validation.

@jgrandja jgrandja modified the milestones: 5.2.0, 5.2.x Oct 19, 2018
@jgrandja jgrandja modified the milestones: 5.2.x, 5.2.0.M1 Oct 30, 2018
@gburboz
Copy link

gburboz commented Nov 20, 2018

@jgrandja clock skew is just one of the many factors due to which this check would fail (e.g. network latency). I'd recommend to name this client config as issuedWithin

Additionally, clock skew is applicable not just to issuedAt but also to expiresAt. I'd recommend to have additional provider level property allowedClockSkew with some default value which can be overridden from config and be used anywhere such comparison is required.

Instant now = Instant.now();
...
Instant maxExpiresAt = now.plusSeconds(allowedClockSkew);
...
Instant maxIssuedAt = now.plusSeconds(issuedWithin + allowedClockSkew);
...

@jgrandja
Copy link
Contributor

jgrandja commented Nov 20, 2018

@gburboz Thanks for the feedback. I'll consider your points when I get to this issue.

@jgrandja jgrandja added type: enhancement A general enhancement in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 13, 2018
@jgrandja jgrandja added the OIDC label Dec 13, 2018
@yannick-fernand
Copy link

Hi @jgrandja i have the same issue. when do you think you will resolve it.
thanks

@jgrandja
Copy link
Contributor

jgrandja commented Jan 7, 2019

@yannick-fernand This is scheduled for 5.2.0.M1 as indicated in the ticket. However, given that M1 is scheduled for Jan 15, it might not make it until M2. I'm trying to catch up with things since the break. I'll do my best.

@jgrandja
Copy link
Contributor

jgrandja commented Jan 9, 2019

@zcwang3 @gburboz @yannick-fernand Clock skew can now be configured via OidcIdTokenValidator.setClockSkew(). However, it's not that easy to configure at the moment as it will require some code duplication on the user's end. This is being addressed in #6379. I'm in the process of putting together a PR for this. After #6379 is resolved, it should be fairly straight forward to configure a clock skew per ClientRegistration - which implicitly supports per provider.

@jgrandja
Copy link
Contributor

jgrandja commented Jan 15, 2019

@zcwang3 @gburboz @yannick-fernand #6379 has been resolved which allows the user to configure the clock skew (per provider) in a straight forward way. The following example demonstrates a custom clock skew setting for google and okta - all other providers default to 60.

@Configuration
public class OAuth2LoginConfig {

	@Bean
	public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
		OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();

		idTokenDecoderFactory.setJwtValidatorFactory(clientRegistration -> {
			OidcIdTokenValidator idTokenValidator = new OidcIdTokenValidator(clientRegistration);
			if (clientRegistration.getRegistrationId().equals("google")) {
				idTokenValidator.setClockSkew(Duration.ofSeconds(30));
			} else if (clientRegistration.getRegistrationId().equals("okta")) {
				idTokenValidator.setClockSkew(Duration.ofSeconds(45));
			}
			return idTokenValidator;
		});

		return idTokenDecoderFactory;
	}
}

NOTE: Please be aware that OidcIdTokenDecoderFactory may be renamed depending on the outcome of #6425. This will be decided before 5.2.0.RC1.

@OdDev26
Copy link

OdDev26 commented Jun 10, 2024

Hello @jgrandja, how can this new method be used?: idTokenDecoderFactory()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

6 participants