Skip to content
This repository was archived by the owner on May 31, 2022. It is now read-only.

Support for PKCE in server as per OAUTH PKCE RFC #675

Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2006-2016 the original author or authors.
*
* 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 org.springframework.security.oauth2.common.exceptions;

/**
* @author Marco Lenzo
*/
@SuppressWarnings("serial")
public class InvalidCodeVerifierException extends ClientAuthenticationException {

public InvalidCodeVerifierException(String msg, Throwable t) {
super(msg, t);
}

public InvalidCodeVerifierException(String msg) {
super(msg);
}

@Override
public String getOAuth2ErrorCode() {
return "invalid_code_verifier";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2006-2016 the original author or authors.
*
* 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 org.springframework.security.oauth2.common.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import org.apache.commons.codec.binary.Base64;
import org.springframework.security.crypto.codec.Utf8;

/**
* Utility class used to parse code verifiers into code challenges as per OAuth PKCE.
* @author Marco Lenzo
*
*/
public class CodeChallengeUtils {

private CodeChallengeUtils() {

}

/**
* Generates the code challenge from a given code verifier and code challenge method.
* @param codeVerifier
* @param codeChallengeMethod allowed values are only <code>plain</code> and <code>S256</code>
* @return
*/
public static String getCodeChallenge(String codeVerifier, String codeChallengeMethod) {
if (codeChallengeMethod.equals("plain")) {
return codeVerifier;
}
else if (codeChallengeMethod.equalsIgnoreCase("S256")) {
return getS256CodeChallenge(codeVerifier);
}
else {
throw new IllegalArgumentException(codeChallengeMethod + " is not a supported code challenge method.");
}
}

private static String getS256CodeChallenge(String codeVerifier) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
}
catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm [SHA-256]");
}
byte[] sha256 = md.digest(Utf8.encode(codeVerifier));
String codeChallenge = Base64.encodeBase64URLSafeString(sha256);
return codeChallenge;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
/**
* @author Ryan Heaton
* @author Dave Syer
* @authos Marco Lenzo
*/
public abstract class OAuth2Utils {

Expand Down Expand Up @@ -72,6 +73,21 @@ public abstract class OAuth2Utils {
*/
public static final String GRANT_TYPE = "grant_type";

/**
* Constant to use while parsing and formatting parameter maps for OAuth2 requests
*/
public static final String CODE_CHALLENGE = "code_challenge";

/**
* Constant to use while parsing and formatting parameter maps for OAuth2 requests
*/
public static final String CODE_CHALLENGE_METHOD = "code_challenge_method";

/**
* Constant to use while parsing and formatting parameter maps for OAuth2 requests
*/
public static final String CODE_VERIFIER = "code_verifier";

/**
* Parses a string parameter value into a set of strings.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.InvalidCodeVerifierException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException;
import org.springframework.security.oauth2.common.util.CodeChallengeUtils;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
Expand All @@ -38,6 +40,7 @@
* Token granter for the authorization code grant type.
*
* @author Dave Syer
* @author Marco Lenzo
*
*/
public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
Expand Down Expand Up @@ -91,6 +94,19 @@ protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, Tok
throw new InvalidClientException("Client ID mismatch");
}

// Code challenge checks
String codeChallenge = pendingOAuth2Request.getRequestParameters().get(OAuth2Utils.CODE_CHALLENGE);
String codeChallengeMethod = pendingOAuth2Request.getRequestParameters().get(OAuth2Utils.CODE_CHALLENGE_METHOD);
String codeVerifier = tokenRequest.getRequestParameters().get(OAuth2Utils.CODE_VERIFIER);
if (codeChallenge != null && codeChallengeMethod != null) {
if(codeVerifier == null) {
throw new InvalidCodeVerifierException("Code verifier expected.");
}
else if (!CodeChallengeUtils.getCodeChallenge(codeVerifier, codeChallengeMethod).equals(codeChallenge)) {
throw new InvalidCodeVerifierException(codeVerifier + " does not match expected code verifier.");
}
}

// 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
// in the token request, but that happens elsewhere.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2006-2011 the original author or authors.
*
* 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 org.springframework.security.oauth2.common.utils;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.springframework.security.oauth2.common.util.CodeChallengeUtils;

/**
* @author Marco Lenzo
*/
public class CodeChallengeUtilsTests {

@Test
public void testPlainCodeChallenge() {
String codeVerifier = "plainCodeChallenge";
String codeChallenge = CodeChallengeUtils.getCodeChallenge("plainCodeChallenge", "plain");
assertEquals(codeChallenge, codeVerifier);
}

@Test
public void testS256CodeChallenge() {
// As per example RFC7636 Appendix B example:
// Code verifier is dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// Code challenge is E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
String codeVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
String codeChallenge = CodeChallengeUtils.getCodeChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"S256");
assertEquals("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", codeChallenge);
}

@Test(expected = IllegalArgumentException.class)
public void testCodeChallengeWithUnsupportedCodeChallengeMethod() {
CodeChallengeUtils.getCodeChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", "xyz");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

package org.springframework.security.oauth2.provider.code;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -23,6 +27,7 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidCodeVerifierException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
Expand All @@ -38,10 +43,6 @@
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
* @author Dave Syer
*
Expand Down Expand Up @@ -93,6 +94,118 @@ public void testAuthorizationCodeGrant() {
assertTrue(providerTokenServices.loadAuthentication(token.getValue()).isAuthenticated());
}

@Test
public void testAuthorizationCodeGrantWithPlainCodeChallenge() {

Authentication userAuthentication = new UsernamePasswordAuthenticationToken("marissa", "koala",
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));

parameters.clear();
parameters.put(OAuth2Utils.CLIENT_ID, "foo");
parameters.put(OAuth2Utils.SCOPE, "scope");
parameters.put(OAuth2Utils.CODE_CHALLENGE, "plainCodeChallenge");
parameters.put(OAuth2Utils.CODE_CHALLENGE_METHOD, "plain");
OAuth2Request storedOAuth2Request = RequestTokenFactory.createOAuth2Request(parameters, "foo", true,
Collections.singleton("scope"));

String code = authorizationCodeServices.createAuthorizationCode(new OAuth2Authentication(storedOAuth2Request,
userAuthentication));
parameters.putAll(storedOAuth2Request.getRequestParameters());
parameters.put("code", code);
parameters.put(OAuth2Utils.CODE_VERIFIER, "plainCodeChallenge");

TokenRequest tokenRequest = requestFactory.createTokenRequest(parameters, client);

AuthorizationCodeTokenGranter granter = new AuthorizationCodeTokenGranter(providerTokenServices,
authorizationCodeServices, clientDetailsService, requestFactory);
OAuth2AccessToken token = granter.grant("authorization_code", tokenRequest);
assertTrue(providerTokenServices.loadAuthentication(token.getValue()).isAuthenticated());
}

@Test(expected = InvalidCodeVerifierException.class)
public void testAuthorizationCodeGrantWithInvalidPlainCodeVerifier() {

Authentication userAuthentication = new UsernamePasswordAuthenticationToken("marissa", "koala",
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));

parameters.clear();
parameters.put(OAuth2Utils.CLIENT_ID, "foo");
parameters.put(OAuth2Utils.SCOPE, "scope");
parameters.put(OAuth2Utils.CODE_CHALLENGE, "plainCodeChallenge");
parameters.put(OAuth2Utils.CODE_CHALLENGE_METHOD, "plain");
OAuth2Request storedOAuth2Request = RequestTokenFactory.createOAuth2Request(parameters, "foo", true,
Collections.singleton("scope"));

String code = authorizationCodeServices.createAuthorizationCode(new OAuth2Authentication(storedOAuth2Request,
userAuthentication));
parameters.putAll(storedOAuth2Request.getRequestParameters());
parameters.put("code", code);
parameters.put(OAuth2Utils.CODE_VERIFIER, "aDifferentPlainCodeVerifier");

TokenRequest tokenRequest = requestFactory.createTokenRequest(parameters, client);

AuthorizationCodeTokenGranter granter = new AuthorizationCodeTokenGranter(providerTokenServices,
authorizationCodeServices, clientDetailsService, requestFactory);
OAuth2AccessToken token = granter.grant("authorization_code", tokenRequest);
}

@Test
public void testAuthorizationCodeGrantWithS256CodeChallenge() {

Authentication userAuthentication = new UsernamePasswordAuthenticationToken("marissa", "koala",
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));

parameters.clear();
parameters.put(OAuth2Utils.CLIENT_ID, "foo");
parameters.put(OAuth2Utils.SCOPE, "scope");
parameters.put(OAuth2Utils.CODE_CHALLENGE, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
parameters.put(OAuth2Utils.CODE_CHALLENGE_METHOD, "S256");
OAuth2Request storedOAuth2Request = RequestTokenFactory.createOAuth2Request(parameters, "foo", true,
Collections.singleton("scope"));

String code = authorizationCodeServices.createAuthorizationCode(new OAuth2Authentication(storedOAuth2Request,
userAuthentication));
parameters.putAll(storedOAuth2Request.getRequestParameters());
parameters.put("code", code);
parameters.put(OAuth2Utils.CODE_VERIFIER, "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");

TokenRequest tokenRequest = requestFactory.createTokenRequest(parameters, client);

AuthorizationCodeTokenGranter granter = new AuthorizationCodeTokenGranter(providerTokenServices,
authorizationCodeServices, clientDetailsService, requestFactory);
OAuth2AccessToken token = granter.grant("authorization_code", tokenRequest);
assertTrue(providerTokenServices.loadAuthentication(token.getValue()).isAuthenticated());
}

@Test(expected = InvalidCodeVerifierException.class)
public void testAuthorizationCodeGrantWithInvalidS256CodeVerifier() {

Authentication userAuthentication = new UsernamePasswordAuthenticationToken("marissa", "koala",
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));

parameters.clear();
parameters.put(OAuth2Utils.CLIENT_ID, "foo");
parameters.put(OAuth2Utils.SCOPE, "scope");
parameters.put(OAuth2Utils.CODE_CHALLENGE, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
parameters.put(OAuth2Utils.CODE_CHALLENGE_METHOD, "S256");
OAuth2Request storedOAuth2Request = RequestTokenFactory.createOAuth2Request(parameters, "foo", true,
Collections.singleton("scope"));

String code = authorizationCodeServices.createAuthorizationCode(new OAuth2Authentication(storedOAuth2Request,
userAuthentication));
parameters.putAll(storedOAuth2Request.getRequestParameters());
parameters.put("code", code);
parameters.put(OAuth2Utils.CODE_VERIFIER, "aaaaaaaaaaaa-mB92K27uhbUJU1p1r_wW1gFWFOEjXa");

TokenRequest tokenRequest = requestFactory.createTokenRequest(parameters, client);

AuthorizationCodeTokenGranter granter = new AuthorizationCodeTokenGranter(providerTokenServices,
authorizationCodeServices, clientDetailsService, requestFactory);
OAuth2AccessToken token = granter.grant("authorization_code", tokenRequest);
assertTrue(providerTokenServices.loadAuthentication(token.getValue()).isAuthenticated());
}


@Test
public void testAuthorizationParametersPreserved() {

Expand Down