OAuth 2.1 PKCE

This commit is contained in:
Crystal.Sea
2021-10-10 15:37:42 +08:00
parent eca3367610
commit 8b3c035102
13 changed files with 138 additions and 34 deletions

View File

@@ -89,9 +89,21 @@ public class OAuth2Constants {
public static final String CODE_CHALLENGE_METHOD = "code_challenge_method" ;
public static final String CODE_VERIFIER = "code_verifier" ;
}
public static class PKCE_TYPE{
public static final String PKCE_TYPE_YES = "YES" ;
public static final String PKCE_TYPE_NO = "NO" ;
}
public static class CODE_CHALLENGE_METHOD_TYPE{
public static final String PLAIN = "plain" ;
public static final String S256 = "S256" ;
}
public static class ENDPOINT{
public final static String ENDPOINT_BASE = "/authz/oauth/v20";

View File

@@ -164,7 +164,7 @@ public class AuthorizationRequest extends BaseRequest implements Serializable {
}
public OAuth2Request createOAuth2Request() {
return new OAuth2Request(getRequestParameters(), getClientId(), getAuthorities(), isApproved(), getScope(), getResourceIds(), getRedirectUri(), getResponseTypes(), getExtensions());
return new OAuth2Request(getRequestParameters(), getClientId(), getAuthorities(), isApproved(), getScope(), getResourceIds(), getRedirectUri(), getResponseTypes(), getCodeChallenge(),getCodeChallengeMethod(),getExtensions());
}
/**

View File

@@ -102,6 +102,7 @@ public class OAuth2Request extends BaseRequest implements Serializable {
public OAuth2Request(Map<String, String> requestParameters, String clientId,
Collection<? extends GrantedAuthority> authorities, boolean approved, Set<String> scope,
Set<String> resourceIds, String redirectUri, Set<String> responseTypes,
String codeChallenge,String codeChallengeMethod,
Map<String, Serializable> extensionProperties) {
setClientId(clientId);
setRequestParameters(requestParameters);
@@ -117,6 +118,8 @@ public class OAuth2Request extends BaseRequest implements Serializable {
if (responseTypes != null) {
this.responseTypes = new HashSet<String>(responseTypes);
}
this.codeChallenge = codeChallenge;
this.codeChallengeMethod = codeChallengeMethod;
this.redirectUri = redirectUri;
if (extensionProperties != null) {
this.extensions = extensionProperties;
@@ -125,8 +128,9 @@ public class OAuth2Request extends BaseRequest implements Serializable {
protected OAuth2Request(OAuth2Request other) {
this(other.getRequestParameters(), other.getClientId(), other.getAuthorities(), other.isApproved(), other
.getScope(), other.getResourceIds(), other.getRedirectUri(), other.getResponseTypes(), other
.getExtensions());
.getScope(), other.getResourceIds(), other.getRedirectUri(), other.getResponseTypes(),
other.getCodeChallenge(),other.getCodeChallengeMethod(),
other.getExtensions());
}
protected OAuth2Request(String clientId) {
@@ -177,7 +181,7 @@ public class OAuth2Request extends BaseRequest implements Serializable {
*/
public OAuth2Request createOAuth2Request(Map<String, String> parameters) {
return new OAuth2Request(parameters, getClientId(), authorities, approved, getScope(), resourceIds,
redirectUri, responseTypes, extensions);
redirectUri, responseTypes, codeChallenge, codeChallengeMethod,extensions);
}
/**
@@ -189,14 +193,14 @@ public class OAuth2Request extends BaseRequest implements Serializable {
*/
public OAuth2Request narrowScope(Set<String> scope) {
OAuth2Request request = new OAuth2Request(getRequestParameters(), getClientId(), authorities, approved, scope,
resourceIds, redirectUri, responseTypes, extensions);
resourceIds, redirectUri, responseTypes, codeChallenge, codeChallengeMethod, extensions);
request.refresh = this.refresh;
return request;
}
public OAuth2Request refresh(TokenRequest tokenRequest) {
OAuth2Request request = new OAuth2Request(getRequestParameters(), getClientId(), authorities, approved,
getScope(), resourceIds, redirectUri, responseTypes, extensions);
getScope(), resourceIds, redirectUri, responseTypes, codeChallenge, codeChallengeMethod,extensions);
request.refresh = tokenRequest;
return request;
}

View File

@@ -110,7 +110,7 @@ public class TokenRequest extends BaseRequest {
// Add grant type so it can be retrieved from OAuth2Request
modifiable.put("grant_type", grantType);
return new OAuth2Request(modifiable, client.getClientId(), client.getAuthorities(), true, this.getScope(),
client.getResourceIds(), null, null, null);
client.getResourceIds(), null, null, null, null, null);
}
}

View File

@@ -20,11 +20,14 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.maxkey.authz.oauth2.common.OAuth2Constants;
import org.maxkey.authz.oauth2.common.OAuth2Constants.CODE_CHALLENGE_METHOD_TYPE;
import org.maxkey.authz.oauth2.common.exceptions.InvalidClientException;
import org.maxkey.authz.oauth2.common.exceptions.InvalidGrantException;
import org.maxkey.authz.oauth2.common.exceptions.InvalidRequestException;
import org.maxkey.authz.oauth2.common.exceptions.OAuth2Exception;
import org.maxkey.authz.oauth2.common.exceptions.RedirectMismatchException;
import org.maxkey.authz.oauth2.common.util.OAuth2Utils;
import org.maxkey.authz.oauth2.provider.ClientDetailsService;
import org.maxkey.authz.oauth2.provider.OAuth2Authentication;
import org.maxkey.authz.oauth2.provider.OAuth2Request;
@@ -32,6 +35,8 @@ import org.maxkey.authz.oauth2.provider.OAuth2RequestFactory;
import org.maxkey.authz.oauth2.provider.TokenRequest;
import org.maxkey.authz.oauth2.provider.token.AbstractTokenGranter;
import org.maxkey.authz.oauth2.provider.token.AuthorizationServerTokenServices;
import org.maxkey.constants.ConstantsProtocols;
import org.maxkey.crypto.DigestUtils;
import org.maxkey.entity.apps.oauth2.provider.ClientDetails;
import org.springframework.security.core.Authentication;
@@ -57,13 +62,15 @@ public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = tokenRequest.getRequestParameters();
String authorizationCode = parameters.get("code");
String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);
String authorizationCode = parameters.get(OAuth2Constants.PARAMETER.CODE);
String redirectUri = parameters.get(OAuth2Constants.PARAMETER.REDIRECT_URI);
String codeVerifier = parameters.get(OAuth2Constants.PARAMETER.CODE_VERIFIER);
if (authorizationCode == null) {
throw new InvalidRequestException("An authorization code must be supplied.");
}
//consume AuthorizationCode
logger.trace("consume AuthorizationCode...");
OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
if (storedAuth == null) {
throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
@@ -72,8 +79,9 @@ public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
// https://jira.springsource.org/browse/SECOAUTH-333
// This might be null, if the authorization was done without the redirect_uri parameter
String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
OAuth2Utils.REDIRECT_URI);
String redirectUriApprovalParameter =
pendingOAuth2Request.getRequestParameters().get(
OAuth2Constants.PARAMETER.REDIRECT_URI);
String pendingClientId = pendingOAuth2Request.getClientId();
String clientId = tokenRequest.getClientId();
@@ -85,7 +93,7 @@ public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
*/
Set<String> redirectUris = client.getRegisteredRedirectUri();
boolean redirectMismatch=false;
//match the stored RedirectUri with request redirectUri parameter
for(String storedRedirectUri : redirectUris){
if(redirectUri.startsWith(storedRedirectUri)){
redirectMismatch=true;
@@ -95,8 +103,8 @@ public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
if ((redirectUri != null || redirectUriApprovalParameter != null)
&& !redirectMismatch) {
logger.info("storedAuth redirectUri "+pendingOAuth2Request.getRedirectUri());
logger.info("redirectUri "+ redirectUri);
logger.info("storedRedirectUri "+ redirectUris);
logger.info("redirectUri parameter "+ redirectUri);
logger.info("stored RedirectUri "+ redirectUris);
throw new RedirectMismatchException("Redirect URI mismatch.");
}
/*
@@ -112,6 +120,31 @@ public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
// just a sanity check.
throw new InvalidClientException("Client ID mismatch");
}
//OAuth 2.1 and PKCE Support
logger.debug("client Protocol "+client.getProtocol()+", PKCE Support "+
(client.getPkce().equalsIgnoreCase(OAuth2Constants.PKCE_TYPE.PKCE_TYPE_YES)));
if(client.getProtocol().equalsIgnoreCase(ConstantsProtocols.OAUTH21)
|| client.getPkce().equalsIgnoreCase(OAuth2Constants.PKCE_TYPE.PKCE_TYPE_YES)) {
logger.trace("stored CodeChallengeMethod "+ pendingOAuth2Request.getCodeChallengeMethod());
logger.trace("stored CodeChallenge "+ pendingOAuth2Request.getCodeChallenge());
logger.trace("stored codeVerifier "+ codeVerifier);
if(StringUtils.isBlank(codeVerifier)) {
throw new OAuth2Exception("code_verifier can not null.");
}
if(StringUtils.isBlank(pendingOAuth2Request.getCodeChallenge())) {
throw new OAuth2Exception("code_challenge can not null.");
}
if(CODE_CHALLENGE_METHOD_TYPE.S256.equalsIgnoreCase(pendingOAuth2Request.getCodeChallengeMethod())) {
codeVerifier = DigestUtils.digestBase64Url(codeVerifier,DigestUtils.Algorithm.SHA256);
}
if(!codeVerifier.equals(pendingOAuth2Request.getCodeChallenge())) {
throw new OAuth2Exception("code_verifier not match.");
}
}
// Secret is not required in the authorization request, so it won't be available
// in the pendingAuthorizationRequest. We do want to check that a secret is provided

View File

@@ -123,8 +123,11 @@ public class AuthorizationEndpoint extends AbstractEndpoint {
@ApiOperation(value = "OAuth 2.0 认证接口", notes = "传递参数client_id,response_type,redirect_uri等",httpMethod="GET")
@RequestMapping(value = OAuth2Constants.ENDPOINT.ENDPOINT_AUTHORIZE, method = RequestMethod.GET)
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus) {
public ModelAndView authorize(
Map<String, Object> model,
@RequestParam Map<String, String> parameters,
SessionStatus sessionStatus) {
Principal principal=(Principal)WebContext.getAuthentication();
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
@@ -152,7 +155,7 @@ public class AuthorizationEndpoint extends AbstractEndpoint {
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Constants.PARAMETER.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
logger.info("Client redirectUri "+resolvedRedirect);
@@ -202,8 +205,11 @@ public class AuthorizationEndpoint extends AbstractEndpoint {
}
@RequestMapping(value = OAuth2Constants.ENDPOINT.ENDPOINT_AUTHORIZE, method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model,
SessionStatus sessionStatus) {
public View approveOrDeny(
@RequestParam Map<String, String> approvalParameters,
Map<String, ?> model,
SessionStatus sessionStatus) {
Principal principal=(Principal)WebContext.getAuthentication();
if (!(principal instanceof Authentication)) {
sessionStatus.setComplete();
@@ -233,8 +239,12 @@ public class AuthorizationEndpoint extends AbstractEndpoint {
}
if (!authorizationRequest.isApproved()) {
return new RedirectView(getUnsuccessfulRedirect(authorizationRequest,
new UserDeniedAuthorizationException("User denied access"), responseTypes.contains(OAuth2Constants.PARAMETER.TOKEN)),
return new RedirectView(
getUnsuccessfulRedirect(
authorizationRequest,
new UserDeniedAuthorizationException("User denied access"),
responseTypes.contains(OAuth2Constants.PARAMETER.TOKEN)
),
false, true, false);
}
@@ -268,12 +278,24 @@ public class AuthorizationEndpoint extends AbstractEndpoint {
if (accessToken == null) {
throw new UnsupportedResponseTypeException("Unsupported response type: token");
}
return new ModelAndView(new RedirectView(appendAccessToken(authorizationRequest, accessToken), false, true,
false));
return new ModelAndView(
new RedirectView(
appendAccessToken(authorizationRequest, accessToken),
false,
true,
false
)
);
}
catch (OAuth2Exception e) {
return new ModelAndView(new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, true), false,
true, false));
return new ModelAndView(
new RedirectView(
getUnsuccessfulRedirect(authorizationRequest, e, true),
false,
true,
false
)
);
}
}
@@ -331,7 +353,7 @@ public class AuthorizationEndpoint extends AbstractEndpoint {
url.append("&").append(templateUrlVar(OAuth2Constants.PARAMETER.EXPIRES_IN));
vars.put(OAuth2Constants.PARAMETER.EXPIRES_IN, expires_in);
}
String originalScope = authorizationRequest.getRequestParameters().get(OAuth2Utils.SCOPE);
String originalScope = authorizationRequest.getRequestParameters().get(OAuth2Constants.PARAMETER.SCOPE);
if (originalScope == null || !OAuth2Utils.parseParameterList(originalScope).equals(accessToken.getScope())) {
url.append("&").append(templateUrlVar(OAuth2Constants.PARAMETER.SCOPE));
vars.put(OAuth2Constants.PARAMETER.SCOPE, OAuth2Utils.formatParameterList(accessToken.getScope()));

View File

@@ -157,9 +157,9 @@ public class TokenEndpoint extends AbstractEndpoint {
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Constants.PARAMETER.SCOPE)));
}
//granter grant access token
token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());

View File

@@ -161,7 +161,7 @@ public class DefaultAccessTokenConverter implements AccessTokenConverter {
authorities = AuthorityUtils.createAuthorityList(roles);
}
OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,
null);
null, null, null);
return new OAuth2Authentication(request, user);
}