Commit f2e51110 authored by michael.simon's avatar michael.simon
Browse files

OIDC: add PKCE support for public SPA

https://tools.ietf.org/html/rfc7636
parent d7e37193
......@@ -42,6 +42,12 @@ public class OidcFlowStateEntity extends AbstractBaseEntity {
@Column(name = "code", length = 256)
private String code;
@Column(name = "code_challange", length = 512)
private String codeChallange;
@Column(name = "code_challange_method", length = 64)
private String codecodeChallangeMethod;
@Column(name = "response_type", length = 256)
private String responseType;
......@@ -171,4 +177,20 @@ public class OidcFlowStateEntity extends AbstractBaseEntity {
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getCodeChallange() {
return codeChallange;
}
public void setCodeChallange(String codeChallange) {
this.codeChallange = codeChallange;
}
public String getCodecodeChallangeMethod() {
return codecodeChallangeMethod;
}
public void setCodecodeChallangeMethod(String codecodeChallangeMethod) {
this.codecodeChallangeMethod = codecodeChallangeMethod;
}
}
......@@ -11,7 +11,9 @@ import net.minidev.json.JSONObject;
public interface OidcOpLogin {
String registerAuthRequest(String realm, String responseType, String redirectUri, String scope, String state,
String nonce, String clientId, HttpServletRequest request, HttpServletResponse response)
String nonce, String clientId,
String codeChallange, String codeChallangeMethod,
HttpServletRequest request, HttpServletResponse response)
throws IOException, OidcAuthenticationException ;
JSONObject serveUserInfo(String realm, String tokeType, String tokenId, HttpServletRequest request,
......@@ -23,6 +25,6 @@ public interface OidcOpLogin {
JSONObject serveUserJwt(String realm, HttpServletRequest request, HttpServletResponse response) throws OidcAuthenticationException;
JSONObject serveToken(String realm, String grantType, String code, String redirectUri, HttpServletRequest request,
HttpServletResponse response, String clientId, String clientSecret) throws OidcAuthenticationException;
HttpServletResponse response, String clientId, String clientSecret, String codeVerifier) throws OidcAuthenticationException;
}
package edu.kit.scc.webreg.service.oidc;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.UUID;
......@@ -28,6 +32,7 @@ import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
......@@ -104,8 +109,11 @@ public class OidcOpLoginImpl implements OidcOpLogin {
public String registerAuthRequest(String realm, String responseType,
String redirectUri, String scope,
String state, String nonce, String clientId,
String codeChallange, String codeChallangeMethod,
HttpServletRequest request, HttpServletResponse response) throws IOException, OidcAuthenticationException {
checkCodeChallange(codeChallange, codeChallangeMethod);
OidcOpConfigurationEntity opConfig = opDao.findByRealmAndHost(realm, request.getServerName());
if (opConfig == null) {
......@@ -123,6 +131,12 @@ public class OidcOpLoginImpl implements OidcOpLogin {
throw new OidcAuthenticationException("unknown client");
}
if (clientConfig.getGenericStore().containsKey("redirect_uri_regex")) {
if (! redirectUri.matches(clientConfig.getGenericStore().get("redirect_uri_regex"))) {
throw new OidcAuthenticationException("invalid redirect uri");
}
}
OidcFlowStateEntity flowState = flowStateDao.createNew();
flowState.setOpConfiguration(opConfig);
flowState.setNonce(nonce);
......@@ -132,6 +146,8 @@ public class OidcOpLoginImpl implements OidcOpLogin {
flowState.setCode(UUID.randomUUID().toString());
flowState.setRedirectUri(redirectUri);
flowState.setValidUntil(new Date(System.currentTimeMillis() + (30L * 60L * 1000L)));
flowState.setCodeChallange(codeChallange);
flowState.setCodecodeChallangeMethod(codeChallangeMethod);
flowState = flowStateDao.persist(flowState);
session.setOidcFlowStateId(flowState.getId());
......@@ -264,7 +280,8 @@ public class OidcOpLoginImpl implements OidcOpLogin {
@Override
public JSONObject serveToken(String realm, String grantType,
String code, String redirectUri,
HttpServletRequest request, HttpServletResponse response, String clientId, String clientSecret) throws OidcAuthenticationException {
HttpServletRequest request, HttpServletResponse response,
String clientId, String clientSecret, String codeVerifier) throws OidcAuthenticationException {
OidcFlowStateEntity flowState = flowStateDao.findByCode(code);
......@@ -284,9 +301,31 @@ public class OidcOpLoginImpl implements OidcOpLogin {
throw new OidcAuthenticationException("unknown client");
}
if ((! clientConfig.getName().equals(clientId)) || (! clientConfig.getSecret().equals(clientSecret))) {
if (! clientConfig.getName().equals(clientId)) {
throw new OidcAuthenticationException("unauthorized");
}
if (clientSecret != null && (! clientConfig.getSecret().equals(clientSecret))) {
// client_id and client_secret is set, but secret is wrong
throw new OidcAuthenticationException("unauthorized");
}
else if (codeVerifier != null) {
//check code verifier
// code_verifier must be SHA256(flowState.getCodeChallange)
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(
codeVerifier.getBytes(StandardCharsets.UTF_8));
String checkStr = new String(Base64.getUrlEncoder().withoutPadding().encode(encodedhash));
if (! checkStr.equals(flowState.getCodeChallange())) {
logger.debug("Code challange failed: {} <-> {}", checkStr, flowState.getCodeChallange());
throw new OidcAuthenticationException("code verification failed");
}
} catch (NoSuchAlgorithmException e) {
throw new OidcAuthenticationException("cannot create hash at the moment. This is bad.");
}
}
IdentityEntity identity = flowState.getIdentity();
......@@ -556,5 +595,19 @@ public class OidcOpLoginImpl implements OidcOpLogin {
}
return returnList;
}
}
private void checkCodeChallange(String codeChallange, String codeChallangeMethod)
throws OidcAuthenticationException {
if (codeChallange != null) {
if (codeChallange.length() > 511) {
throw new OidcAuthenticationException("Code challange is not acceptable");
}
}
if (codeChallangeMethod != null) {
if (! CodeChallengeMethod.S256.toString().equals(codeChallangeMethod)) {
throw new OidcAuthenticationException("Code challange method is not supported");
}
}
}
}
......@@ -25,10 +25,12 @@ public class OidcAuthorizationController {
public void auth(@PathParam("realm") String realm, @QueryParam("response_type") String responseType,
@QueryParam("redirect_uri") String redirectUri, @QueryParam("scope") String scope,
@QueryParam("state") String state, @QueryParam("nonce") String nonce, @QueryParam("client_id") String clientId,
@QueryParam("code_challenge") String codeChallange, @QueryParam("code_challenge_method") String codeChallangeMethod,
@Context HttpServletRequest request, @Context HttpServletResponse response)
throws IOException, OidcAuthenticationException {
String red = opLogin.registerAuthRequest(realm, responseType, redirectUri, scope, state, nonce, clientId, request, response);
String red = opLogin.registerAuthRequest(realm, responseType, redirectUri, scope, state, nonce, clientId,
codeChallange, codeChallangeMethod, request, response);
response.sendRedirect(red);
}
......
......@@ -33,13 +33,14 @@ public class OidcTokenController {
public JSONObject auth(@PathParam("realm") String realm, @FormParam("grant_type") String grantType,
@FormParam("code") String code, @FormParam("redirect_uri") String redirectUri,
@FormParam("client_id") String clientId, @FormParam("client_secret") String clientSecret,
@FormParam("code_verifier") String codeVerifier,
@Context HttpServletRequest request, @Context HttpServletResponse response)
throws Exception {
logger.debug("Post token called for {} with code {} and grant_type {}", realm, code, grantType);
if (clientId != null && clientSecret != null) {
return opLogin.serveToken(realm, grantType, code, redirectUri, request, response, clientId, clientSecret);
if (clientId != null && (clientSecret != null || codeVerifier != null)) {
return opLogin.serveToken(realm, grantType, code, redirectUri, request, response, clientId, clientSecret, codeVerifier);
}
String auth = request.getHeader("Authorization");
......@@ -51,7 +52,7 @@ public class OidcTokenController {
new String(Base64.decodeBase64(auth.substring(index).getBytes())), ":", 2);
if (credentials.length == 2) {
return opLogin.serveToken(realm, grantType, code, redirectUri, request, response, credentials[0], credentials[1]);
return opLogin.serveToken(realm, grantType, code, redirectUri, request, response, credentials[0], credentials[1], codeVerifier);
}
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment