Skip to content
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

Expire OAuth2AuthorizationRequest when saving to the session #9513

Conversation

candrews
Copy link
Contributor

Closes gh-5145

This straightforward approach cleans up expired OAuth2AuthorizationRequests as they're loaded (and subsequently saved) to the session. It's basically the same approach as #7381 but storing creation instead of expiration and using a wrapper class instead of modifying OAuth2AuthorizationRequest.

This approach introduces no new dependencies or requirements (such as Spring Cache abstraction).

From #7381 (review)

The expiring capability is really a cross-cutting concern. Can you try the following solution?

* Intercept calls to `loadAuthorizationRequest()` and store the information necessary

* At a scheduled time (configurable with skew), the component will attempt to `loadAuthorizationRequest()` and if it returns than determine if time has expired and than `removeAuthorizationRequest`

Please take a look at the Cache abstraction to see if it would meet our needs.

For HttpSessionOAuth2AuthorizationRequestRepository, I don't believe that could work because the session can only be loaded and saved while the request for it is being processed. In other words, the task that runs at the scheduled itme would not be able to call loadAuthorizationRequest() and get the data back, nor would it be able to call removeAuthorizationRequest to write it. Even if it could call those methods, they wouldn't work right; session persistence is handed by servlet filters (see Spring Session) and those filters wouldn't be intercepting session use to handle persistence outside of a request.

It make sense to create a new implementation of the AuthorizationRequestRepository<OAuth2AuthorizationRequest> interface at some point which uses a database or Spring Cache abstraction for persistence, as that would allow for expiration to run as a scheduled job. However, that's not of interest to me at the moment :)

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 23, 2021
@candrews candrews force-pushed the expire-OAuth2AuthorizationRequest branch from dd27350 to 9d3429f Compare March 23, 2021 02:18
@candrews candrews force-pushed the expire-OAuth2AuthorizationRequest branch 3 times, most recently from 045afac to ffcb332 Compare March 23, 2021 19:46
@candrews candrews force-pushed the expire-OAuth2AuthorizationRequest branch from ffcb332 to 58fb623 Compare March 23, 2021 20:33
@candrews
Copy link
Contributor Author

@jgrandja can you please take a look at this PR?

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @candrews ! Please see review comments.

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Mar 26, 2021
@jgrandja jgrandja added this to the 5.5.0-RC1 milestone Mar 26, 2021
@candrews
Copy link
Contributor Author

Thanks for the PR @candrews ! Please see review comments.

I've addressed feedback in a fixup commit I added. I'm assuming that the commits will be squashed when the PR is merged.

Thank you for your review!

@candrews
Copy link
Contributor Author

@jgrandja Is there anything else you'd like me to do?

Thanks again

@candrews
Copy link
Contributor Author

candrews commented Apr 5, 2021

I've updated this PR to also set a maximum number of OAuth2AuthorizationRequest per session, which is important based on my own testing and was suggested at #5145 (comment)

@jgrandja can you please take a peek?

@jgrandja
Copy link
Contributor

jgrandja commented Apr 5, 2021

@candrews Thanks. I will look at this shortly.

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates @candrews. Please see review comments.

* @param clock the clock
* @since 5.5
*/
void setClock(Clock clock) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this as it's not needed. The clock will always be in sync since this implementation operates independently within it's own application instance. Clock and clock skew is needed when components are interacting in a distributed way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is package private, so nothing other than Spring itself should be using it.

Being able to set the clock is important for testing. See https://github.com/candrews/spring-security/blob/9b1a85d992f3ac0d725f67ac03ee6d9f44659b21/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java#L249

Is there another approach that should be taken to make the clock available to be set by tests?

* attempt is made to save another one, then the oldest will be removed.
* @param maxActiveAuthorizationRequests must not be negative.
*/
public void setMaxActiveAuthorizationRequestsPerSession(int maxActiveAuthorizationRequestsPerSession) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the specific use case why this is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to easily put a lot of OAuth2AuthorizationRequests in this map, resulting in Bad Things ™️ happening. This limit prevents that from being possible.

I contacted security@pivotal.io with the details. Let me know if I should post them here.

@candrews candrews force-pushed the expire-OAuth2AuthorizationRequest branch from 1a5cf56 to 5f98510 Compare April 6, 2021 20:46
@jgrandja jgrandja modified the milestones: 5.5.0-RC1, 5.5.x Apr 9, 2021
@candrews
Copy link
Contributor Author

candrews commented Apr 9, 2021

How's this looking now?

@jgrandja
Copy link
Contributor

@candrews I've been giving this enhancement some further thought and I would prefer to keep this a patch change.

Something about setAuthorizationRequestTimeToLive(), setMaxActiveAuthorizationRequestsPerSession(), and setClock() doesn't feel right from a design standpoint.

Furthermore, I'm not sure how much these configuration-related settings will be used given that gh-5145 did not receive any upvotes and the issue is now over 3 years old.

HttpSessionOAuth2AuthorizationRequestRepository is designed to be a simplified (default) version of AuthorizationRequestRepository. However, if an application requires more advanced capabilities than it can configure a custom AuthorizationRequestRepository via oauth2Client() or oauth2Login().

Can you provide me more details on your use case and why you need this enhancement?

@candrews
Copy link
Contributor Author

Something about setAuthorizationRequestTimeToLive(), setMaxActiveAuthorizationRequestsPerSession(), and setClock() doesn't feel right from a design standpoint.

Furthermore, I'm not sure how much these configuration-related settings will be used given that gh-5145 did not receive any upvotes and the issue is now over 3 years old.

I'm happy to change those methods to be package visible (so they can be used by tests, unless you have a better approach for changing them to facilitating testing) instead of public. I don't really care if they're configurable, and as you said, I doubt anyone else would either.

Can you provide me more details on your use case and why you need this enhancement?

I don't care about the configurability of the expiration or size parameters, but I do think having these limits is very important.

As it stands now, Spring Security sets no limits, allowing the map to grow boundlessly. As @rwinch says:

It is technically a memory leak and sessions can be kept around quite a long time.

-- #5145 (comment)

I'm trying to not give away too much; having this map grow boundlessly is a real significant problem. If you'd like me to give the specifics, let me know and I will.

Change setMaxActiveAuthorizationRequestsPerSession and setAuthorizationRequestTimeToLive to be package visible, not public.
@jgrandja
Copy link
Contributor

@candrews

If you'd like me to give the specifics, let me know and I will.

No, I'm good. I got the message and much appreciated.

I have another idea brewing for this patch. Let me think it over a bit. I'll get back to you by end of day tomorrow the latest.

@jgrandja
Copy link
Contributor

@candrews Here is a template for the solution.

I've added implementation notes but let me know if you have any questions.

/*
 * 1) OAuth2AuthorizationRequest's are stored as Map<String, OAuth2AuthorizationRequestWrapper>, keyed by getState()
 * 2) When saveAuthorizationRequest(), constrain limit by grouping by OAuth2AuthorizationRequestWrapper.getRegistrationId().
 * 		For example, if there are 3 existing `google-registration-id` then reject the save by throwing IllegalStateException.
 * 		I think constraining to 3 for the limit-based strategy is ok?
 * 3) When loadAuthorizationRequest(), remove all isExpired() before returning the Map
 */
private static class OAuth2AuthorizationRequestWrapper implements Serializable {
	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private static final Duration DEFAULT_TIME_TO_LIVE = Duration.ofMinutes(5);

	private final OAuth2AuthorizationRequest authorizationRequest;

	private final Instant createdAt;

	private final Instant expiresAt;

	private OAuth2AuthorizationRequestWrapper(OAuth2AuthorizationRequest authorizationRequest) {
		this.authorizationRequest = authorizationRequest;
		this.createdAt = Instant.now();
		// I think 5 minutes is long enough to allow for the authorization (consent) flow to complete?
		this.expiresAt = this.createdAt.plus(DEFAULT_TIME_TO_LIVE);
	}

	private OAuth2AuthorizationRequest getAuthorizationRequest() {
		return this.authorizationRequest;
	}

	private String getRegistrationId() {
		return this.authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
	}

	private String getState() {
		return this.authorizationRequest.getState();
	}

	private boolean isExpired() {
		// NOTE:
		// Given that Instant.now() uses the system clock and
		// createdAt and expiresAt is instantiated by the same system clock,
		// there is no need for a configurable Clock or any clock skew adjustment
		return Instant.now().isAfter(this.expiresAt);
	}
}

@candrews
Copy link
Contributor Author

  1. OAuth2AuthorizationRequest's are stored as Map<String, OAuth2AuthorizationRequestWrapper>, keyed by getState()

That's already in place. Based on earlier feedback, the class is named OAuth2AuthorizationRequestReference, not OAuth2AuthorizationRequestWrapper, though.

  1. When saveAuthorizationRequest(), constrain limit by grouping by OAuth2AuthorizationRequestWrapper.getRegistrationId().
    For example, if there are 3 existing google-registration-id then reject the save by throwing IllegalStateException.
    I think constraining to 3 for the limit-based strategy is ok?

If we throw IllegalStateException, then the user won't be able to authorize using that registration id for the remainder of their session or until the 2 minute expiration occurs clearing that OAuth2AuthorizationRequestWrapper from the map.

Perhaps instead of throwing IllegalStateException, we should remove the oldest OAuth2AuthorizationRequestWrapper for that registration id and continue?

I've just made that change and pushed the commit for it.

  1. When loadAuthorizationRequest(), remove all isExpired() before returning the Map

Already done.

I kept setClock though as a package private method, as it facilitates testing. If you have a better approach, please let me know.

Also, the commit history is getting excessive. Would you like me to squash all the commits and force push update this PR?

@candrews candrews force-pushed the expire-OAuth2AuthorizationRequest branch from 9cd5e3b to 5f720ff Compare April 13, 2021 22:19
@candrews
Copy link
Contributor Author

@jgrandja here's an alternative, very simple approach that also addresses the root issue:
#9649

@jgrandja
Copy link
Contributor

@candrews I'll close this one in favour of gh-9649

@jgrandja jgrandja closed this Apr 20, 2021
@jgrandja jgrandja removed this from the 5.5.x milestone Apr 20, 2021
@jgrandja jgrandja added status: duplicate A duplicate of another issue and removed type: enhancement A general enhancement labels Apr 20, 2021
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) status: duplicate A duplicate of another issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

OAuth2AuthorizationRequest should expire
3 participants