com.nimbusds.oauth2.sdk.TokenRequest Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of oauth2-oidc-sdk Show documentation
Show all versions of oauth2-oidc-sdk Show documentation
OAuth 2.0 SDK with OpenID Connection extensions for developing client
and server applications.
/*
* oauth2-oidc-sdk
*
* Copyright 2012-2023, Connect2id Ltd and contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.nimbusds.oauth2.sdk;
import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.rar.AuthorizationDetail;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.oauth2.sdk.util.*;
import net.jcip.annotations.Immutable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
/**
* Token request. Used to obtain an
* {@link com.nimbusds.oauth2.sdk.token.AccessToken access token} and an
* optional {@link com.nimbusds.oauth2.sdk.token.RefreshToken refresh token}
* at the Token endpoint of the authorisation server. Supports custom request
* parameters.
*
* Example token request with an authorisation code grant:
*
*
* POST /token HTTP/1.1
* Host: server.example.com
* Content-Type: application/x-www-form-urlencoded
* Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
*
* grant_type=authorization_code
* &code=SplxlOBeZQQYbYS6WxSbIA
* &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
*
*
* Related specifications:
*
*
* - OAuth 2.0 (RFC 6749), sections 4.1.3, 4.3.2, 4.4.2 and 6.
*
- OAuth 2.0 Rich Authorization Requests (RFC 9396), section 6.
*
- Resource Indicators for OAuth 2.0 (RFC 8707)
*
- OAuth 2.0 Incremental Authorization
* (draft-ietf-oauth-incremental-authz-04)
*
*/
@Immutable
public class TokenRequest extends AbstractOptionallyIdentifiedRequest {
/**
* The authorisation grant.
*/
private final AuthorizationGrant authzGrant;
/**
* The requested scope, {@code null} if not specified.
*/
private final Scope scope;
/**
* The RAR details (optional).
*/
private final List authorizationDetails;
/**
* The resource URI(s), {@code null} if not specified.
*/
private final List resources;
/**
* Existing refresh token for incremental authorisation of a public
* client, {@code null} if not specified.
*/
private final RefreshToken existingGrant;
/**
* Custom request parameters.
*/
private final Map> customParams;
private static final Set ALLOWED_REPEATED_PARAMS = new HashSet<>(Arrays.asList("resource", "audience"));
/**
* Creates a new token request with the specified client
* authentication.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest} method
* will not be used.
* @param clientAuth The client authentication. Must not be
* {@code null}.
* @param authzGrant The authorisation grant. Must not be {@code null}.
* @param scope The requested scope, {@code null} if not
* specified.
*/
public TokenRequest(final URI uri,
final ClientAuthentication clientAuth,
final AuthorizationGrant authzGrant,
final Scope scope) {
this(uri, clientAuth, authzGrant, scope, null, null);
}
/**
* Creates a new token request with the specified client
* authentication and extension and custom parameters.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest}
* method will not be used.
* @param clientAuth The client authentication. Must not be
* {@code null}.
* @param authzGrant The authorisation grant. Must not be
* {@code null}.
* @param scope The requested scope, {@code null} if not
* specified.
* @param resources The resource URI(s), {@code null} if not
* specified.
* @param customParams Custom parameters to be included in the request
* body, empty map or {@code null} if none.
*/
public TokenRequest(final URI uri,
final ClientAuthentication clientAuth,
final AuthorizationGrant authzGrant,
final Scope scope,
final List resources,
final Map> customParams) {
this(uri, clientAuth, authzGrant, scope, null, resources, customParams);
}
/**
* Creates a new token request with the specified client
* authentication and extension and custom parameters.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the
* {@link #toHTTPRequest} method will not
* be used.
* @param clientAuth The client authentication. Must not be
* {@code null}.
* @param authzGrant The authorisation grant. Must not be
* {@code null}.
* @param scope The requested scope, {@code null} if not
* specified.
* @param authorizationDetails The Rich Authorisation Request (RAR)
* details, {@code null} if not specified.
* @param resources The resource URI(s), {@code null} if not
* specified.
* @param customParams Custom parameters to be included in the
* request body, empty map or {@code null}
* if none.
*/
public TokenRequest(final URI uri,
final ClientAuthentication clientAuth,
final AuthorizationGrant authzGrant,
final Scope scope,
final List authorizationDetails,
final List resources,
final Map> customParams) {
super(uri, clientAuth);
if (clientAuth == null)
throw new IllegalArgumentException("The client authentication must not be null");
this.authzGrant = authzGrant;
this.scope = scope;
if (resources != null) {
for (URI resourceURI: resources) {
if (! ResourceUtils.isLegalResourceURI(resourceURI))
throw new IllegalArgumentException("Resource URI must be absolute and with no query or fragment: " + resourceURI);
}
}
this.authorizationDetails = authorizationDetails;
this.resources = resources;
this.existingGrant = null; // only for confidential client
if (MapUtils.isNotEmpty(customParams)) {
this.customParams = customParams;
} else {
this.customParams = Collections.emptyMap();
}
}
/**
* Creates a new token request with the specified client
* authentication.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest} method
* will not be used.
* @param clientAuth The client authentication. Must not be
* {@code null}.
* @param authzGrant The authorisation grant. Must not be {@code null}.
*/
public TokenRequest(final URI uri,
final ClientAuthentication clientAuth,
final AuthorizationGrant authzGrant) {
this(uri, clientAuth, authzGrant, null);
}
/**
* Creates a new token request, with no explicit client authentication
* (maybe present in the grant depending on its type).
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest} method
* will not be used.
* @param clientID The client identifier, {@code null} if not
* specified.
* @param authzGrant The authorisation grant. Must not be {@code null}.
* @param scope The requested scope, {@code null} if not
* specified.
*/
public TokenRequest(final URI uri,
final ClientID clientID,
final AuthorizationGrant authzGrant,
final Scope scope) {
this(uri, clientID, authzGrant, scope, null, null,null);
}
/**
* Creates a new token request, with no explicit client authentication
* (maybe present in the grant depending on its type) and extension
* and custom parameters.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest}
* method will not be used.
* @param clientID The client identifier, {@code null} if not
* specified.
* @param authzGrant The authorisation grant. Must not be
* {@code null}.
* @param scope The requested scope, {@code null} if not
* specified.
* @param resources The resource URI(s), {@code null} if not
* specified.
* @param existingGrant Existing refresh token for incremental
* authorisation of a public client, {@code null}
* if not specified.
* @param customParams Custom parameters to be included in the request
* body, empty map or {@code null} if none.
*/
public TokenRequest(final URI uri,
final ClientID clientID,
final AuthorizationGrant authzGrant,
final Scope scope,
final List resources,
final RefreshToken existingGrant,
final Map> customParams) {
this(uri, clientID, authzGrant, scope, null, resources, existingGrant, customParams);
}
/**
* Creates a new token request, with no explicit client authentication
* (maybe present in the grant depending on its type) and extension
* and custom parameters.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the
* {@link #toHTTPRequest}
* method will not be used.
* @param clientID The client identifier, {@code null} if
* not specified.
* @param authzGrant The authorisation grant. Must not be
* {@code null}.
* @param scope The requested scope, {@code null} if not
* specified.
* @param authorizationDetails The Rich Authorisation Request (RAR)
* details, {@code null} if not specified.
* @param resources The resource URI(s), {@code null} if not
* specified.
* @param existingGrant Existing refresh token for incremental
* authorisation of a public client,
* {@code null} if not specified.
* @param customParams Custom parameters to be included in the
* request body, empty map or {@code null}
* if none.
*/
public TokenRequest(final URI uri,
final ClientID clientID,
final AuthorizationGrant authzGrant,
final Scope scope,
final List authorizationDetails,
final List resources,
final RefreshToken existingGrant,
final Map> customParams) {
super(uri, clientID);
if (authzGrant.getType().requiresClientAuthentication()) {
throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires client authentication");
}
if (authzGrant.getType().requiresClientID() && clientID == null) {
throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires a \"client_id\" parameter");
}
this.authzGrant = authzGrant;
this.scope = scope;
this.authorizationDetails = authorizationDetails;
if (resources != null) {
for (URI resourceURI: resources) {
if (! ResourceUtils.isLegalResourceURI(resourceURI))
throw new IllegalArgumentException("Resource URI must be absolute and with no query or fragment: " + resourceURI);
}
}
this.resources = resources;
this.existingGrant = existingGrant;
if (MapUtils.isNotEmpty(customParams)) {
this.customParams = customParams;
} else {
this.customParams = Collections.emptyMap();
}
}
/**
* Creates a new token request, with no explicit client authentication
* (maybe present in the grant depending on its type).
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest} method
* will not be used.
* @param clientID The client identifier, {@code null} if not
* specified.
* @param authzGrant The authorisation grant. Must not be {@code null}.
*/
public TokenRequest(final URI uri,
final ClientID clientID,
final AuthorizationGrant authzGrant) {
this(uri, clientID, authzGrant, null);
}
/**
* Creates a new token request, without client authentication and a
* specified client identifier.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest} method
* will not be used.
* @param authzGrant The authorisation grant. Must not be {@code null}.
* @param scope The requested scope, {@code null} if not
* specified.
*/
public TokenRequest(final URI uri,
final AuthorizationGrant authzGrant,
final Scope scope) {
this(uri, (ClientID)null, authzGrant, scope);
}
/**
* Creates a new token request, without client authentication and a
* specified client identifier.
*
* @param uri The URI of the token endpoint. May be
* {@code null} if the {@link #toHTTPRequest} method
* will not be used.
* @param authzGrant The authorisation grant. Must not be {@code null}.
*/
public TokenRequest(final URI uri,
final AuthorizationGrant authzGrant) {
this(uri, (ClientID)null, authzGrant, null);
}
/**
* Returns the authorisation grant.
*
* @return The authorisation grant.
*/
public AuthorizationGrant getAuthorizationGrant() {
return authzGrant;
}
/**
* Returns the requested scope.
*
* @return The requested scope, {@code null} if not specified.
*/
public Scope getScope() {
return scope;
}
/**
* Returns the Rich Authorisation Request (RAR) details.
*
* @return The authorisation details, {@code null} if not specified.
*/
public List getAuthorizationDetails() {
return authorizationDetails;
}
/**
* Returns the resource server URI.
*
* @return The resource URI(s), {@code null} if not specified.
*/
public List getResources() {
return resources;
}
/**
* Returns the existing refresh token for incremental authorisation of
* a public client, {@code null} if not specified.
*
* @return The existing grant, {@code null} if not specified.
*/
public RefreshToken getExistingGrant() {
return existingGrant;
}
/**
* Returns the additional custom parameters included in the request
* body.
*
* Example:
*
*
* resource=http://xxxxxx/PartyOData
*
*
* @return The additional custom parameters as an unmodifiable map,
* empty map if none.
*/
public Map> getCustomParameters () {
return Collections.unmodifiableMap(customParams);
}
/**
* Returns the specified custom parameter included in the request body.
*
* @param name The parameter name. Must not be {@code null}.
*
* @return The parameter value(s), {@code null} if not specified.
*/
public List getCustomParameter(final String name) {
return customParams.get(name);
}
@Override
public HTTPRequest toHTTPRequest() {
if (getEndpointURI() == null)
throw new SerializeException("The endpoint URI is not specified");
HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
if (getClientAuthentication() != null) {
getClientAuthentication().applyTo(httpRequest);
}
Map> params;
try {
params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
} catch (ParseException e) {
throw new SerializeException(e.getMessage(), e);
}
params.putAll(authzGrant.toParameters());
switch (authzGrant.getType().getScopeRequirementInTokenRequest()) {
case REQUIRED:
if (CollectionUtils.isEmpty(scope)) {
throw new SerializeException("Scope is required for the " + authzGrant.getType() + " grant");
}
params.put("scope", Collections.singletonList(scope.toString()));
break;
case OPTIONAL:
if (CollectionUtils.isNotEmpty(scope)) {
params.put("scope", Collections.singletonList(scope.toString()));
}
break;
case NOT_ALLOWED:
default:
break;
}
if (getClientID() != null) {
params.put("client_id", Collections.singletonList(getClientID().getValue()));
}
if (getAuthorizationDetails() != null) {
params.put("authorization_details", Collections.singletonList(AuthorizationDetail.toJSONString(getAuthorizationDetails())));
}
if (getResources() != null) {
List values = new LinkedList<>();
for (URI uri: resources) {
if (uri == null)
continue;
values.add(uri.toString());
}
params.put("resource", values);
}
if (getExistingGrant() != null) {
params.put("existing_grant", Collections.singletonList(existingGrant.getValue()));
}
if (! getCustomParameters().isEmpty()) {
params.putAll(getCustomParameters());
}
httpRequest.setBody(URLUtils.serializeParameters(params));
return httpRequest;
}
/**
* Parses a token request from the specified HTTP request.
*
* @param httpRequest The HTTP request. Must not be {@code null}.
*
* @return The token request.
*
* @throws ParseException If the HTTP request couldn't be parsed to a
* token request.
*/
public static TokenRequest parse(final HTTPRequest httpRequest)
throws ParseException {
// Only HTTP POST accepted
URI uri = httpRequest.getURI();
httpRequest.ensureMethod(HTTPRequest.Method.POST);
httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
// Parse client authentication, if any
ClientAuthentication clientAuth;
try {
clientAuth = ClientAuthentication.parse(httpRequest);
} catch (ParseException e) {
throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
}
// No fragment! May use query component!
Map> params = httpRequest.getBodyAsFormParameters();
Set repeatParams = MultivaluedMapUtils.getKeysWithMoreThanOneValue(params, ALLOWED_REPEATED_PARAMS);
if (! repeatParams.isEmpty()) {
String msg = "Parameter(s) present more than once: " + repeatParams;
throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.setDescription(msg));
}
// Multiple conflicting client auth methods (issue #203)?
if (clientAuth instanceof ClientSecretBasic) {
if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) {
String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion";
throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
}
}
// Parse grant
AuthorizationGrant grant = AuthorizationGrant.parse(params);
if (clientAuth == null && grant.getType().requiresClientAuthentication()) {
String msg = "Missing client authentication";
throw new ParseException(msg, OAuth2Error.INVALID_CLIENT.appendDescription(": " + msg));
}
// Parse client id
ClientID clientID = null;
if (clientAuth == null) {
// Parse optional client ID
String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
if (clientIDString != null && ! clientIDString.trim().isEmpty())
clientID = new ClientID(clientIDString);
if (clientID == null && grant.getType().requiresClientID()) {
String msg = "Missing required client_id parameter";
throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
}
}
// Parse optional scope
String scopeValue = MultivaluedMapUtils.getFirstValue(params, "scope");
ParameterRequirement scopeRequirement = grant.getType().getScopeRequirementInTokenRequest();
Scope scope = null;
if (scopeValue != null && (ParameterRequirement.REQUIRED.equals(scopeRequirement) || ParameterRequirement.OPTIONAL.equals(scopeRequirement))) {
scope = Scope.parse(scopeValue);
}
// Parse optional RAR
String json = MultivaluedMapUtils.getFirstValue(params, "authorization_details");
List authorizationDetails = null;
if (json != null) {
authorizationDetails = AuthorizationDetail.parseList(json);
}
// Parse optional resource URIs
List resources = null;
List vList = params.get("resource");
if (vList != null) {
resources = new LinkedList<>();
for (String uriValue: vList) {
if (uriValue == null)
continue;
String errMsg = "Illegal resource parameter: Must be an absolute URI without a fragment: " + uriValue;
URI resourceURI;
try {
resourceURI = new URI(uriValue);
} catch (URISyntaxException e) {
throw new ParseException(errMsg, OAuth2Error.INVALID_TARGET.setDescription(errMsg));
}
if (! ResourceUtils.isLegalResourceURI(resourceURI)) {
throw new ParseException(errMsg, OAuth2Error.INVALID_TARGET.setDescription(errMsg));
}
resources.add(resourceURI);
}
}
String rt = MultivaluedMapUtils.getFirstValue(params, "existing_grant");
RefreshToken existingGrant = StringUtils.isNotBlank(rt) ? new RefreshToken(rt) : null;
// Parse custom parameters
Map> customParams = new HashMap<>();
for (Map.Entry> p: params.entrySet()) {
if (p.getKey().equalsIgnoreCase("grant_type")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("client_id")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("client_secret")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("client_assertion_type")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("client_assertion")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("scope")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("authorization_details")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("resource")) {
continue; // skip
}
if (p.getKey().equalsIgnoreCase("existing_grant"))
continue; // skip
if (! grant.getType().getRequestParameterNames().contains(p.getKey())) {
// We have a custom (non-registered) parameter
customParams.put(p.getKey(), p.getValue());
}
}
if (clientAuth != null) {
return new TokenRequest(uri, clientAuth, grant, scope, authorizationDetails, resources, customParams);
} else {
// public client
return new TokenRequest(uri, clientID, grant, scope, authorizationDetails, resources, existingGrant, customParams);
}
}
}