Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jans-auth-server): updated first party native authn implementation ( in backwards compatibility way) #10380 #10442

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io.jans.as.common.model.common.User;
import io.jans.as.common.model.session.AuthorizationChallengeSession;
import io.jans.as.server.auth.DpopService;
import io.jans.as.server.authorize.ws.rs.AuthorizationChallengeSessionService;
import io.jans.as.server.service.UserService;
import io.jans.as.server.service.external.context.ExternalScriptContext;
Expand Down Expand Up @@ -128,9 +129,15 @@ private AuthorizationChallengeSession prepareAuthorizationChallengeSession(Exter
AuthorizationChallengeSessionService authorizationChallengeSessionService = CdiUtil.bean(AuthorizationChallengeSessionService.class);
boolean newSave = authorizationChallengeSessionObject == null;
if (newSave) {
// authorizationChallengeSessionObject = authorizationChallengeSessionService.newAuthorizationChallengeSession();
authorizationChallengeSessionObject = authorizationChallengeSessionService.newAuthorizationChallengeSession();
}

final String dpop = context.getHttpRequest().getHeader(DpopService.DPOP);
if (StringUtils.isNotBlank(dpop)) {
authorizationChallengeSessionObject.getAttributes().setJkt(getDpopJkt(dpop));
}


String username = context.getHttpRequest().getParameter(USERNAME_PARAMETER);
if (StringUtils.isNotBlank(username)) {
authorizationChallengeSessionObject.getAttributes().getAttributes().put(USERNAME_PARAMETER, username);
Expand Down Expand Up @@ -160,6 +167,19 @@ private AuthorizationChallengeSession prepareAuthorizationChallengeSession(Exter
return authorizationChallengeSessionObject;
}

public String getDpopJkt(String dpop) {
if (StringUtils.isBlank(dpop)) {
return null;
}

try {
return DpopService.getDpopJwkThumbprint(dpop);
} catch (Exception e) {
scriptLogger.error("Failed to get jkt from DPoP: " + dpop,e);
return null;
}
}

private String getParameterFromAuthorizationChallengeSession(ExternalScriptContext context, String parameterName) {
final AuthorizationChallengeSession sessionObject = context.getAuthzRequest().getAuthorizationChallengeSessionObject();
if (sessionObject != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public class AuthorizationChallengeSessionAttributes implements Serializable {
@JsonProperty("acr_values")
private String acrValues;

// jkt - JWK SHA-256 Thumbprint confirmation method.
// The value of the jkt member MUST be the base64url encoding (as defined in [RFC7515]) of the JWK SHA-256 Thumbprint
// (according to [RFC7638]) of the DPoP public key (in JWK format) to which the access token is bound.
@JsonProperty("jkt")
private String jkt;

@JsonProperty("attributes")
private Map<String, String> attributes;

Expand All @@ -38,11 +44,21 @@ public void setAcrValues(String acrValues) {
this.acrValues = acrValues;
}

public String getJkt() {
return jkt;
}

public AuthorizationChallengeSessionAttributes setJkt(String jkt) {
this.jkt = jkt;
return this;
}

@Override
public String toString() {
return "DeviceSessionAttributes{" +
"acrValues='" + acrValues + '\'' +
"attributes='" + attributes + '\'' +
"jkt='" + jkt + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ private Response.ResponseBuilder error(int status, TokenErrorResponseType type,
return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE).entity(errorResponseFactory.errorAsJson(type, reason));
}

public String getDpopJwkThumbprint(String dpopStr) throws InvalidJwtException, NoSuchAlgorithmException, JWKException, NoSuchProviderException {
public static String getDpopJwkThumbprint(String dpopStr) throws InvalidJwtException, NoSuchAlgorithmException, JWKException, NoSuchProviderException {
final Jwt dpop = Jwt.parseOrThrow(dpopStr);
JSONWebKey jwk = JSONWebKey.fromJSONObject(dpop.getHeader().getJwk());
return jwk.getJwkThumbprint();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.jans.as.server.authorize.ws.rs;

import io.jans.as.model.util.QueryStringDecoder;
import io.jans.as.server.auth.DpopService;
import io.jans.as.server.service.RequestParameterService;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -38,6 +39,8 @@ public Response requestAuthorizationPost(
@FormParam("acr_values") String acrValues,
@FormParam("auth_session") String authorizationChallengeSession,
@FormParam("use_auth_session") String useAuthorizationChallengeSession,
@FormParam("device_session") String deviceSession, // old name in draft 00
@FormParam("use_device_session") String useDeviceSession, // old name in draft 00
@FormParam("prompt") String prompt,
@FormParam("state") String state,
@FormParam("nonce") String nonce,
Expand All @@ -63,6 +66,15 @@ public Response requestAuthorizationPost(
authzRequest.setCodeChallenge(codeChallenge);
authzRequest.setCodeChallengeMethod(codeChallengeMethod);
authzRequest.setAuthzDetailsString(authorizationDetails);
authzRequest.setDpop(httpRequest.getHeader(DpopService.DPOP));

// backwards compatibilty: device_session (up to draft 02) vs auth_session (draft 02 and later)
if (authorizationChallengeSession == null && deviceSession != null) {
authzRequest.setAuthorizationChallengeSession(deviceSession);
}
if (useAuthorizationChallengeSession == null && useDeviceSession != null) {
authzRequest.setUseAuthorizationChallengeSession(Boolean.parseBoolean(useDeviceSession));
}

return authorizationChallengeService.requestAuthorization(authzRequest);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public void prepareAuthzRequest(AuthzRequest authzRequest) {
if (StringUtils.isNotBlank(authzRequest.getAuthorizationChallengeSession())) {
final AuthorizationChallengeSession session = authorizationChallengeSessionService.getAuthorizationChallengeSession(authzRequest.getAuthorizationChallengeSession());

authorizationChallengeValidator.validateDpopJkt(session, authzRequest.getDpop());

authzRequest.setAuthorizationChallengeSessionObject(session);
if (session != null) {
final Map<String, String> attributes = session.getAttributes().getAttributes();
Expand Down Expand Up @@ -188,6 +190,7 @@ public Response authorize(AuthzRequest authzRequest) throws IOException, TokenBi
authorizationGrant.setClaims(authzRequest.getClaims());
authorizationGrant.setSessionDn(sessionUser != null ? sessionUser.getDn() : "no_session_for_authorization_challenge"); // no need for session as at Authorization Endpoint
authorizationGrant.setAcrValues(grantAcr);
authorizationGrant.setAuthorizationChallenge(true);
authorizationGrant.save();

String authorizationCode = authorizationGrant.getAuthorizationCode().getCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import java.util.*;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.UUID;

/**
* @author Yuriy Z
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package io.jans.as.server.authorize.ws.rs;

import io.jans.as.common.model.registration.Client;
import io.jans.as.common.model.session.AuthorizationChallengeSession;
import io.jans.as.model.authorize.AuthorizeErrorResponseType;
import io.jans.as.model.common.GrantType;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.model.error.ErrorResponseFactory;
import io.jans.as.model.token.TokenErrorResponseType;
import io.jans.as.server.auth.DpopService;
import io.jans.as.server.model.config.Constants;
import io.jans.as.server.service.ScopeService;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import java.util.Arrays;
Expand All @@ -35,6 +39,30 @@ public class AuthorizationChallengeValidator {
@Inject
private ScopeService scopeService;

public void validateDpopJkt(AuthorizationChallengeSession session, String dpop) {
final String jkt = session.getAttributes().getJkt();
if (StringUtils.isBlank(jkt)) {
return;
}

try {
final String dpopJwkThumbprint = DpopService.getDpopJwkThumbprint(dpop);
if (jkt.equals(dpopJwkThumbprint)) {
return;
} else {
log.debug("Unable to match dpopJkt: {} with sessionJkt: {}", dpopJwkThumbprint, jkt);
}
} catch (Exception e) {
String msg = String.format("Failed to validate dpop jtk. jkt: %s, dpop: %s", jkt, dpop);
log.debug(msg, e);
}

throw new WebApplicationException(errorResponseFactory
.newErrorResponse(Response.Status.BAD_REQUEST)
.entity(errorResponseFactory.getErrorAsJson(TokenErrorResponseType.INVALID_DPOP_PROOF, "", "Invalid DPoP."))
.build());
}

public void validateGrantType(Client client, String state) {
if (client == null) {
final String msg = "Unable to find client.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class AuthzRequest {
private String claims;
private String authReqId;
private String dpopJkt;
private String dpop;
private String authzDetailsString;
private AuthzDetails authzDetails;
private String httpMethod;
Expand Down Expand Up @@ -95,6 +96,15 @@ public void setDpopJkt(String dpopJkt) {
this.dpopJkt = dpopJkt;
}

public String getDpop() {
return dpop;
}

public AuthzRequest setDpop(String dpop) {
this.dpop = dpop;
return this;
}

public AuthorizationChallengeSession getAuthorizationChallengeSessionObject() {
return authorizationChallengeSessionObject;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public abstract class AbstractAuthorizationGrant implements IAuthorizationGrant

private String acrValues;
private String sessionDn;
private boolean isAuthorizationChallenge;

protected final ConcurrentMap<String, TxToken> txTokens = new ConcurrentHashMap<>();
protected final ConcurrentMap<String, AccessToken> accessTokens = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -110,6 +111,15 @@ public void setReferenceId(String referenceId) {
this.referenceId = referenceId;
}

public boolean isAuthorizationChallenge() {
return isAuthorizationChallenge;
}

public AbstractAuthorizationGrant setAuthorizationChallenge(boolean authorizationChallenge) {
isAuthorizationChallenge = authorizationChallenge;
return this;
}

public Integer getStatusListIndex() {
return statusListIndex;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ private void initTokenFromGrant(TokenEntity token) {
}

token.getAttributes().setAuthorizationDetails(getAuthzDetailsAsString());
token.getAttributes().setAuthorizationChallenge(isAuthorizationChallenge());
token.setScope(getScopesAsString());
token.setAuthMode(getAcrValues());
token.setSessionDn(getSessionDn());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) {
result.setTokenEntity(tokenEntity);
result.setReferenceId(tokenEntity.getReferenceId());
result.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex());
result.setAuthorizationChallenge(tokenEntity.getAttributes().isAuthorizationChallenge());
if (StringUtils.isNotBlank(grantId)) {
result.setGrantId(grantId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ grantType, code, redirectUri, username, refreshToken, clientId, prepareForLogs(r
scope = ServerUtil.urlDecode(scope); // it may be encoded in uma case

try {
tokenRestWebServiceValidator.validateParams(grantType, code, redirectUri, refreshToken, auditLog);
tokenRestWebServiceValidator.validateParams(grantType, code, refreshToken, auditLog);

GrantType gt = GrantType.fromString(grantType);
log.debug("Grant type: '{}'", gt);
Expand All @@ -212,7 +212,7 @@ grantType, code, redirectUri, username, refreshToken, clientId, prepareForLogs(r
executionContext.setAuthzDetails(authzDetails);

if (gt == GrantType.AUTHORIZATION_CODE) {
return processAuthorizationCode(code, scope, codeVerifier, sessionIdObj, executionContext);
return processAuthorizationCode(code, scope, codeVerifier, sessionIdObj, redirectUri, executionContext);
} else if (gt == GrantType.REFRESH_TOKEN) {
return processRefreshTokenGrant(scope, refreshToken, idTokenPreProcessing, executionContext);
} else if (gt == GrantType.CLIENT_CREDENTIALS) {
Expand Down Expand Up @@ -434,14 +434,19 @@ private TokenEntity lockAndRemoveRefreshToken(String refreshTokenCode) {
return null;
}

private Response processAuthorizationCode(String code, String scope, String codeVerifier, SessionId sessionIdObj, ExecutionContext executionContext) {
private Response processAuthorizationCode(String code, String scope, String codeVerifier, SessionId sessionIdObj, String redirectUri, ExecutionContext executionContext) {
Client client = executionContext.getClient();

log.debug("Attempting to find authorizationCodeGrant by clientId: '{}', code: '{}'", client.getClientId(), code);
final AuthorizationCodeGrant authorizationCodeGrant = authorizationGrantList.getAuthorizationCodeGrant(code);
executionContext.setGrant(authorizationCodeGrant);
log.trace("AuthorizationCodeGrant : '{}'", authorizationCodeGrant);

// validate redirectUri only for Authorization Code Flow. For First-Party App redirect uri is blank. It is perfectly valid case.
if (!authorizationCodeGrant.isAuthorizationChallenge()) {
tokenRestWebServiceValidator.validateRedirectUri(redirectUri, executionContext.getAuditLog());
}

// if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code
tokenRestWebServiceValidator.validateGrant(authorizationCodeGrant, client, code, executionContext.getAuditLog(), grant -> grantService.removeAllByAuthorizationCode(code));
tokenRestWebServiceValidator.validatePKCE(authorizationCodeGrant, codeVerifier, executionContext.getAuditLog());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public void validatePKCE(AuthorizationCodeGrant grant, String codeVerifier, OAut
}

public void validateParams(String grantType, String code,
String redirectUri, String refreshToken, OAuth2AuditLog auditLog) {
String refreshToken, OAuth2AuditLog auditLog) {
log.debug("Starting to validate request parameters");
if (grantType == null || grantType.isEmpty()) {
final String msg = "Grant Type is not set.";
Expand All @@ -98,11 +98,6 @@ public void validateParams(String grantType, String code,
log.trace(msg);
throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_REQUEST, msg), auditLog));
}
if (StringUtils.isBlank(redirectUri)) {
final String msg = "redirect_uri is not set for AUTHORIZATION_CODE.";
log.trace(msg);
throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_REQUEST, msg), auditLog));
}
return;
}

Expand Down Expand Up @@ -173,6 +168,14 @@ public void validateGrant(AuthorizationGrant grant, Client client, Object identi
validateGrant(grant, client, identifier, auditLog, null);
}

public void validateRedirectUri(String redirectUri, OAuth2AuditLog auditLog) {
if (StringUtils.isBlank(redirectUri)) {
final String msg = "redirect_uri is not set for AUTHORIZATION_CODE.";
log.trace(msg);
throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_REQUEST, msg), auditLog));
}
}


public void validateGrant(AuthorizationGrant grant, Client client, Object identifier, OAuth2AuditLog auditLog, Consumer<AuthorizationGrant> onFailure) {
if (grant == null) {
Expand Down
Loading
Loading