Skip to content
Merged
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
62 changes: 62 additions & 0 deletions src/main/java/com/auth0/AuthenticationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,66 @@ public AuthorizeUrl buildAuthorizeUrl(HttpServletRequest request, HttpServletRes
return requestProcessor.buildAuthorizeUrl(request, response, redirectUri, state, nonce);
}

/**
* Builds a request to exchange a refresh token for a new set of {@link Tokens}, optionally
* targeting a specific audience and/or scope. This exposes Auth0's refresh-token grant,
* enabling Multi-Resource Refresh Token (MRRT) flows where one refresh token can obtain access
* tokens for multiple APIs.
*
* <p>The application supplies the {@code domain} it stored from {@link Tokens#getDomain()} at
* login. This is required because a refresh can occur outside of an HTTP request, where the
* domain cannot otherwise be resolved. For applications configured with a fixed domain, the
* {@link AuthenticationController#renewAuth(String)} overload may be used instead.</p>
*
* @param refreshToken the refresh token to exchange.
* @param domain the Auth0 domain to target.
* @return a {@link RenewAuthRequest} to configure and execute.
*/
public RenewAuthRequest renewAuth(String refreshToken, String domain) {
Validate.notNull(refreshToken, "refreshToken must not be null");
Validate.notNull(domain, "domain must not be null");
return requestProcessor.buildRenewAuthRequest(refreshToken, domain);
}

/**
* Builds a request to exchange a refresh token for a new set of {@link Tokens} using the
* statically configured domain. See {@link AuthenticationController#renewAuth(String, String)}
* for details.
*
* <p>This overload is only valid when the controller was configured with a fixed domain. When a
* {@code DomainResolver} is in use, call {@link AuthenticationController#renewAuth(String, String)}
* with the domain instead.</p>
*
* @param refreshToken the refresh token to exchange.
* @return a {@link RenewAuthRequest} to configure and execute.
* @throws IllegalStateException if the controller was configured with a {@code DomainResolver}.
*/
public RenewAuthRequest renewAuth(String refreshToken) {
Validate.notNull(refreshToken, "refreshToken must not be null");
return requestProcessor.buildRenewAuthRequest(refreshToken);
}

/**
* Builds a request to exchange a refresh token for a new set of {@link Tokens}, resolving the
* Auth0 domain from the given request via the configured domain or {@code DomainResolver}.
* See {@link AuthenticationController#renewAuth(String, String)} for details.
*
* <p>This overload works for both a fixed domain and a {@code DomainResolver}, and is convenient
* when refreshing within an active request. <strong>Note:</strong> a refresh token is bound to
* the domain it was issued for at login; if the resolver resolves the given request to a
* different domain, Auth0 will reject the grant. Use this overload only when the request
* resolves to the same domain as login; otherwise use
* {@link AuthenticationController#renewAuth(String, String)} with the domain stored from
* {@link Tokens#getDomain()} at login.</p>
*
* @param refreshToken the refresh token to exchange.
* @param request the current HTTP request, used to resolve the domain.
* @return a {@link RenewAuthRequest} to configure and execute.
*/
public RenewAuthRequest renewAuth(String refreshToken, HttpServletRequest request) {
Validate.notNull(refreshToken, "refreshToken must not be null");
Validate.notNull(request, "request must not be null");
return requestProcessor.buildRenewAuthRequest(refreshToken, request);
}

}
88 changes: 88 additions & 0 deletions src/main/java/com/auth0/RenewAuthRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.auth0;

import com.auth0.client.auth.AuthAPI;
import com.auth0.exception.Auth0Exception;
import com.auth0.json.auth.TokenHolder;
import com.auth0.net.TokenRequest;

/**
* Class to exchange a refresh token for a new set of {@link Tokens}, optionally targeting a
* specific {@code audience} and/or {@code scope}. This exposes Auth0's refresh-token grant,
* enabling Multi-Resource Refresh Token (MRRT) flows where one refresh token can obtain access
* tokens for multiple APIs.
* <p>
* The library remains stateless: the application owns storage of the refresh token, caching of
* the resulting access tokens, and any concurrency control around refresh-token rotation.
* <p>
* Obtain an instance via {@link AuthenticationController#renewAuth(String, String)},
* {@link AuthenticationController#renewAuth(String)}, or
* {@link AuthenticationController#renewAuth(String, jakarta.servlet.http.HttpServletRequest)}.
*/
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused"})
public class RenewAuthRequest {

private final AuthAPI client;
private final String refreshToken;
private final String domain;
private final String issuer;
private String audience;
private String scope;

RenewAuthRequest(AuthAPI client, String refreshToken, String domain, String issuer) {
this.client = client;
this.refreshToken = refreshToken;
this.domain = domain;
this.issuer = issuer;
}

/**
* Sets the audience to request an access token for. When not set, Auth0 uses the default
* audience configured for the application.
* <p>
* Note: if the requested audience is not permitted by the application's MRRT policy, Auth0
* does not error; it returns a token for the default audience instead. Callers must verify
* the {@code aud} claim of the returned access token.
*
* @param audience the audience (API identifier) to request a token for.
* @return this request instance for fluent chaining.
*/
public RenewAuthRequest withAudience(String audience) {
this.audience = audience;
return this;
}

/**
* Sets the scope to request for the access token.
*
* @param scope the requested scope.
* @return this request instance for fluent chaining.
*/
public RenewAuthRequest withScope(String scope) {
this.scope = scope;
return this;
}

/**
* Executes the refresh-token grant against Auth0 and returns the resulting tokens.
* <p>
* The refresh-token grant does not return an ID token, so {@link Tokens#getIdToken()} is
* typically null. When refresh-token rotation is enabled, the returned
* {@link Tokens#getRefreshToken()} is a new refresh token that supersedes the one used here;
* the application is responsible for persisting it.
*
* @return the {@link Tokens} obtained from the grant, including the granted scope.
* @throws Auth0Exception if the request to the Auth0 server failed.
*/
public Tokens execute() throws Auth0Exception {
TokenRequest request = client.renewAuth(refreshToken);
if (audience != null) {
request.setAudience(audience);
}
if (scope != null) {
request.setScope(scope);
}
TokenHolder holder = request.execute().getBody();
return new Tokens(holder.getAccessToken(), holder.getIdToken(), holder.getRefreshToken(),
holder.getTokenType(), holder.getExpiresIn(), holder.getScope(), domain, issuer);
}
}
50 changes: 48 additions & 2 deletions src/main/java/com/auth0/RequestProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,49 @@ AuthAPI createClientForDomain(String domain) {
.build();
}

/**
* Builds a {@link RenewAuthRequest} to exchange a refresh token for new tokens against the
* given domain. The domain is supplied explicitly because a refresh can occur outside of an
* HTTP request (e.g. a background refresh), where the {@link DomainProvider} cannot resolve it.
*
* @param refreshToken the refresh token to exchange.
* @param domain the Auth0 domain to target.
* @return a {@link RenewAuthRequest} ready to configure and execute.
*/
RenewAuthRequest buildRenewAuthRequest(String refreshToken, String domain) {
AuthAPI client = createClientForDomain(domain);
String issuer = constructIssuer(domain);
return new RenewAuthRequest(client, refreshToken, domain, issuer);
}

/**
* Builds a {@link RenewAuthRequest} using the statically configured domain. Only valid when the
* controller was configured with a fixed domain; when a {@link DomainResolver} is in use there
* is no fixed domain to target and the domain must be supplied explicitly.
*
* @param refreshToken the refresh token to exchange.
* @return a {@link RenewAuthRequest} ready to configure and execute.
* @throws IllegalStateException if the controller was configured with a {@link DomainResolver}.
*/
RenewAuthRequest buildRenewAuthRequest(String refreshToken) {
if (!(domainProvider instanceof StaticDomainProvider)) {
throw new IllegalStateException("A domain is required when using a DomainResolver; call renewAuth(refreshToken, domain).");
}
return buildRenewAuthRequest(refreshToken, domainProvider.getDomain(null));
}

/**
* Builds a {@link RenewAuthRequest} resolving the domain from the given request via the
* configured {@link DomainProvider}. Works for both a fixed domain and a {@link DomainResolver}.
*
* @param refreshToken the refresh token to exchange.
* @param request the current HTTP request, used to resolve the domain.
* @return a {@link RenewAuthRequest} ready to configure and execute.
*/
RenewAuthRequest buildRenewAuthRequest(String refreshToken, HttpServletRequest request) {
return buildRenewAuthRequest(refreshToken, domainProvider.getDomain(request));
}

private Auth0HttpClient getHttpClient() {
if (this.httpClient == null) {
DefaultHttpClient.Builder httpBuilder = DefaultHttpClient.newBuilder()
Expand Down Expand Up @@ -450,7 +493,7 @@ private Tokens exchangeCodeForTokens(String authorizationCode, String redirectUr
.execute()
.getBody();
String originIssuer = constructIssuer(originDomain);
return new Tokens(holder.getAccessToken(), holder.getIdToken(), holder.getRefreshToken(), holder.getTokenType(), holder.getExpiresIn(), originDomain, originIssuer);
return new Tokens(holder.getAccessToken(), holder.getIdToken(), holder.getRefreshToken(), holder.getTokenType(), holder.getExpiresIn(), holder.getScope(), originDomain, originIssuer);
}

/**
Expand All @@ -470,15 +513,18 @@ private Tokens mergeTokens(Tokens frontChannelTokens, Tokens codeExchangeTokens)
String accessToken;
String type;
Long expiresIn;
String scope;

if (codeExchangeTokens.getAccessToken() != null) {
accessToken = codeExchangeTokens.getAccessToken();
type = codeExchangeTokens.getType();
expiresIn = codeExchangeTokens.getExpiresIn();
scope = codeExchangeTokens.getScope();
} else {
accessToken = frontChannelTokens.getAccessToken();
type = frontChannelTokens.getType();
expiresIn = frontChannelTokens.getExpiresIn();
scope = frontChannelTokens.getScope();
}

// Prefer ID token from the front-channel
Expand All @@ -493,7 +539,7 @@ private Tokens mergeTokens(Tokens frontChannelTokens, Tokens codeExchangeTokens)
String issuer = frontChannelTokens.getIssuer() != null ? frontChannelTokens.getIssuer()
: codeExchangeTokens.getIssuer();

return new Tokens(accessToken, idToken, refreshToken, type, expiresIn, domain, issuer);
return new Tokens(accessToken, idToken, refreshToken, type, expiresIn, scope, domain, issuer);
}

private String constructIssuer(String domain) {
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/auth0/Tokens.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class Tokens implements Serializable {
private final String refreshToken;
private final String type;
private final Long expiresIn;
private final String scope;
private final String domain;
private final String issuer;

Expand Down Expand Up @@ -49,11 +50,29 @@ public Tokens(String accessToken, String idToken, String refreshToken, String ty
* @param issuer the issuer URL from the ID token
*/
public Tokens(String accessToken, String idToken, String refreshToken, String type, Long expiresIn, String domain, String issuer) {
this(accessToken, idToken, refreshToken, type, expiresIn, null, domain, issuer);
}

/**
* Full constructor including the granted scope.
*
* @param accessToken access token for Auth0 API
* @param idToken identity token with user information
* @param refreshToken refresh token that can be used to request new tokens
* without signing in again
* @param type token type
* @param expiresIn token expiration
* @param scope the scope granted for the access token, or null if not provided
* @param domain the Auth0 domain that issued these tokens
* @param issuer the issuer URL from the ID token
*/
public Tokens(String accessToken, String idToken, String refreshToken, String type, Long expiresIn, String scope, String domain, String issuer) {
this.accessToken = accessToken;
this.idToken = idToken;
this.refreshToken = refreshToken;
this.type = type;
this.expiresIn = expiresIn;
this.scope = scope;
this.domain = domain;
this.issuer = issuer;
}
Expand Down Expand Up @@ -103,6 +122,15 @@ public Long getExpiresIn() {
return expiresIn;
}

/**
* Getter for the scope granted for the Access Token.
*
* @return the granted scope, or null if not provided.
*/
public String getScope() {
return scope;
}


/**
* Getter for the Auth0 domain that issued these tokens.
Expand Down
Loading