Skip to content

Commit

Permalink
feat(jans-auth-server): Draft support of OpenID Connect Native SSO (#…
Browse files Browse the repository at this point in the history
…2711)

* feat(jans-auth-server): added device secret generation

#2518

* feat(jans-auth-server): do not process request if token exchange grant type is not present

Native SSO

#2518

* fix(jans-auth-server): sonar smell fix

Native SSO

#2518

* feat(jans-auth-server): added validation of subject token type and actor token type

Native SSO

#2518

* feat(jans-auth-server): added token exchange grant

Native SSO

#2518

* fix(jans-auth-server): sonar smell - removed empty constructor

Native SSO

#2518

* fix(jans-auth-server): introduced TokenExchangeService

Native SSO

#2518

* feat(jans-auth-server): added deviceSecret to indexes

Native SSO

#2518

* feat(jans-auth-server): draft implementation of native sso

Native SSO

#2518
  • Loading branch information
yuriyz authored Oct 28, 2022
1 parent 5ece137 commit 595d1aa
Show file tree
Hide file tree
Showing 28 changed files with 1,003 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,7 @@
import java.util.List;
import java.util.Map;

import static io.jans.as.model.authorize.AuthorizeResponseParam.ACCESS_TOKEN;
import static io.jans.as.model.authorize.AuthorizeResponseParam.AUD;
import static io.jans.as.model.authorize.AuthorizeResponseParam.CODE;
import static io.jans.as.model.authorize.AuthorizeResponseParam.EXP;
import static io.jans.as.model.authorize.AuthorizeResponseParam.EXPIRES_IN;
import static io.jans.as.model.authorize.AuthorizeResponseParam.ID_TOKEN;
import static io.jans.as.model.authorize.AuthorizeResponseParam.ISS;
import static io.jans.as.model.authorize.AuthorizeResponseParam.RESPONSE;
import static io.jans.as.model.authorize.AuthorizeResponseParam.SCOPE;
import static io.jans.as.model.authorize.AuthorizeResponseParam.SESSION_ID;
import static io.jans.as.model.authorize.AuthorizeResponseParam.SID;
import static io.jans.as.model.authorize.AuthorizeResponseParam.STATE;
import static io.jans.as.model.authorize.AuthorizeResponseParam.TOKEN_TYPE;
import static io.jans.as.model.authorize.AuthorizeResponseParam.*;

/**
* Represents an authorization response received from the authorization server.
Expand All @@ -64,6 +52,7 @@ public class AuthorizationResponse extends BaseResponse {
private String state;
private String sessionId;
private String sid;
private String deviceSecret;
private Map<String, String> customParams;
private ResponseMode responseMode;

Expand Down Expand Up @@ -203,6 +192,7 @@ private void processLocation() {
}
}

@SuppressWarnings("java:S3776")
private void loadParams(Map<String, String> params) throws UnsupportedEncodingException {
if (params.containsKey(CODE)) {
code = params.get(CODE);
Expand All @@ -216,6 +206,10 @@ private void loadParams(Map<String, String> params) throws UnsupportedEncodingEx
sid = params.get(SID);
params.remove(SID);
}
if (params.containsKey(DEVICE_SECRET)) {
deviceSecret = params.get(DEVICE_SECRET);
params.remove(DEVICE_SECRET);
}
if (params.containsKey(ACCESS_TOKEN)) {
accessToken = params.get(ACCESS_TOKEN);
params.remove(ACCESS_TOKEN);
Expand Down Expand Up @@ -319,6 +313,14 @@ public void setSid(String sid) {
this.sid = sid;
}

public String getDeviceSecret() {
return deviceSecret;
}

public void setDeviceSecret(String deviceSecret) {
this.deviceSecret = deviceSecret;
}

/**
* Gets session id.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
import org.jetbrains.annotations.NotNull;

import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.*;

/**
* @author Yuriy Zabrovarnyy
Expand Down Expand Up @@ -76,6 +74,9 @@ public class SessionId implements Deletable, Serializable {
@AttributeName(name = "jansSessAttr")
private Map<String, String> sessionAttributes;

@AttributeName(name = "deviceSecret")
private List<String> deviceSecrets;

@AttributeName(name = "exp")
private Date expirationDate;

Expand All @@ -94,6 +95,16 @@ public class SessionId implements Deletable, Serializable {
@Expiration
private int ttl;

@NotNull
public List<String> getDeviceSecrets() {
if (deviceSecrets == null) deviceSecrets = new ArrayList<>();
return deviceSecrets;
}

public void setDeviceSecrets(List<String> deviceSecrets) {
this.deviceSecrets = deviceSecrets;
}

public int getTtl() {
return ttl;
}
Expand Down Expand Up @@ -305,6 +316,7 @@ public String toString() {
sb.append(", permissionGrantedMap=").append(permissionGrantedMap);
sb.append(", sessionAttributes=").append(sessionAttributes);
sb.append(", persisted=").append(persisted);
sb.append(", deviceSecrets=").append(deviceSecrets);
sb.append("}");
return sb.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public final class AuthorizeResponseParam {
public static final String ISS = "iss";
public static final String AUD = "aud";
public static final String EXP = "exp";
public static final String DEVICE_SECRET = "device_secret";

/**
* String that represents the End-User's login state at the OP.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public enum GrantType implements HasParamName, AttributeEnum {
*/
OXAUTH_UMA_TICKET("urn:ietf:params:oauth:grant-type:uma-ticket"),

/**
* Token exchange grant type for OAuth 2.0
*/
TOKEN_EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange"),

/**
* CIBA (Client Initiated Backchannel Authentication) Grant Type.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class ScopeConstants {

public static final String OPENID = "openid";
public static final String OFFLINE_ACCESS = "offline_access";
public static final String DEVICE_SSO = "device_sso";

private ScopeConstants() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ private Constants() {
public static final String NO_CACHE = "no-cache";
public static final String X_CLIENTCERT = "X-ClientCert";
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
public static final String SUBJECT_TOKEN_TYPE_ID_TOKEN = "urn:ietf:params:oauth:token-type:id_token";
public static final String ACTOR_TOKEN_TYPE_DEVICE_SECRET = "urn:x-oath:params:oauth:token-type:device-secret";
public static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";

public static final String CONTENT_TYPE_APPLICATION_JSON_UTF_8 = "application/json;charset=UTF-8";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ public class AppConfiguration implements Configuration {
private Boolean openidScopeBackwardCompatibility = false;
private Boolean disableU2fEndpoint = false;

// Token Exchange
private Boolean rotateDeviceSecret = false;
private Boolean returnDeviceSecretFromAuthzEndpoint = false;

// DCR
private Boolean dcrSignatureValidationEnabled = false;
private String dcrSignatureValidationSharedSecret;
private String dcrSignatureValidationSoftwareStatementJwksURIClaim;
Expand Down Expand Up @@ -345,6 +350,23 @@ public void setAllowAllValueForRevokeEndpoint(Boolean allowAllValueForRevokeEndp
this.allowAllValueForRevokeEndpoint = allowAllValueForRevokeEndpoint;
}

public Boolean getReturnDeviceSecretFromAuthzEndpoint() {
return returnDeviceSecretFromAuthzEndpoint;
}

public void setReturnDeviceSecretFromAuthzEndpoint(Boolean returnDeviceSecretFromAuthzEndpoint) {
this.returnDeviceSecretFromAuthzEndpoint = returnDeviceSecretFromAuthzEndpoint;
}

public Boolean getRotateDeviceSecret() {
if (rotateDeviceSecret == null) rotateDeviceSecret = false;
return rotateDeviceSecret;
}

public void setRotateDeviceSecret(Boolean rotateDeviceSecret) {
this.rotateDeviceSecret = rotateDeviceSecret;
}

public Boolean getRequirePkce() {
if (requirePkce == null) requirePkce = false;
return requirePkce;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@
@Path("/")
public class AuthorizeRestWebServiceImpl implements AuthorizeRestWebService {

private static final String SUCCESSFUL_RP_REDIRECT_COUNT = "successful_rp_redirect_count";

@Inject
private Logger log;

Expand Down Expand Up @@ -502,7 +504,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx
ResponseBuilder builder = RedirectUtil.getRedirectResponseBuilder(authzRequest.getRedirectUriResponse().getRedirectUri(), authzRequest.getHttpRequest());

addCustomHeaders(builder, authzRequest);
updateSessionRpRedirect(sessionUser);
updateSession(authzRequest, sessionUser);

runCiba(authzRequest.getAuthReqId(), client, authzRequest.getHttpRequest(), authzRequest.getHttpResponse());
processDeviceAuthorization(deviceAuthzUserCode, user);
Expand Down Expand Up @@ -910,11 +912,13 @@ private Response redirectTo(String pathToRedirect, AuthzRequest authzRequest, Li
return builder.build();
}

private void updateSessionRpRedirect(SessionId sessionUser) {
int rpRedirectCount = Util.parseIntSilently(sessionUser.getSessionAttributes().get("successful_rp_redirect_count"), 0);
private void updateSession(AuthzRequest authzRequest, SessionId sessionUser) {
authzRequestService.addDeviceSecretToSession(authzRequest, sessionUser);

int rpRedirectCount = Util.parseIntSilently(sessionUser.getSessionAttributes().get(SUCCESSFUL_RP_REDIRECT_COUNT), 0);
rpRedirectCount++;

sessionUser.getSessionAttributes().put("successful_rp_redirect_count", Integer.toString(rpRedirectCount));
sessionUser.getSessionAttributes().put(SUCCESSFUL_RP_REDIRECT_COUNT, Integer.toString(rpRedirectCount));
sessionIdService.updateSessionId(sessionUser);
}

Expand All @@ -925,7 +929,7 @@ private boolean unauthenticateSession(String sessionId, HttpServletRequest httpR
private boolean unauthenticateSession(String sessionId, HttpServletRequest httpRequest, boolean isPromptFromJwt) {
SessionId sessionUser = identity.getSessionId();

if (isPromptFromJwt && sessionUser != null && !sessionUser.getSessionAttributes().containsKey("successful_rp_redirect_count")) {
if (isPromptFromJwt && sessionUser != null && !sessionUser.getSessionAttributes().containsKey(SUCCESSFUL_RP_REDIRECT_COUNT)) {
return false; // skip unauthentication because there were no at least one successful rp redirect
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
import com.google.common.collect.Sets;
import io.jans.as.common.model.common.User;
import io.jans.as.common.model.registration.Client;
import io.jans.as.common.model.session.SessionId;
import io.jans.as.common.util.CommonUtils;
import io.jans.as.common.util.RedirectUri;
import io.jans.as.model.authorize.AuthorizeErrorResponseType;
import io.jans.as.model.authorize.AuthorizeResponseParam;
import io.jans.as.model.common.GrantType;
import io.jans.as.model.common.Prompt;
import io.jans.as.model.common.ResponseMode;
import io.jans.as.model.common.ScopeConstants;
import io.jans.as.model.config.WebKeysConfiguration;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.model.crypto.AbstractCryptoProvider;
Expand All @@ -34,6 +38,7 @@
import io.jans.as.server.model.authorize.IdTokenMember;
import io.jans.as.server.model.authorize.JwtAuthorizationRequest;
import io.jans.as.server.model.authorize.ScopeChecker;
import io.jans.as.server.model.token.HandleTokenFactory;
import io.jans.as.server.par.ws.rs.ParService;
import io.jans.as.server.service.*;
import io.jans.as.server.util.ServerUtil;
Expand All @@ -44,6 +49,7 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
Expand All @@ -52,6 +58,7 @@
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -101,6 +108,27 @@ public class AuthzRequestService {
@Inject
private RedirectionUriService redirectionUriService;

public void addDeviceSecretToSession(AuthzRequest authzRequest, SessionId sessionId) {
if (BooleanUtils.isFalse(appConfiguration.getReturnDeviceSecretFromAuthzEndpoint())) {
return;
}
if (!Arrays.asList(authzRequest.getScope().split(" ")).contains(ScopeConstants.DEVICE_SSO)) {
return;
}
if (!ArrayUtils.contains(authzRequest.getClient().getGrantTypes(), GrantType.TOKEN_EXCHANGE)) {
log.debug("Skip device secret. Scope has {} value but client does not have Token Exchange Grant Type enabled ('urn:ietf:params:oauth:grant-type:token-exchange')", ScopeConstants.DEVICE_SSO);
return;
}

final String newDeviceSecret = HandleTokenFactory.generateDeviceSecret();

final List<String> deviceSecrets = sessionId.getDeviceSecrets();
deviceSecrets.add(newDeviceSecret);

authzRequest.getRedirectUriResponse().getRedirectUri().addResponseParameter(AuthorizeResponseParam.DEVICE_SECRET, newDeviceSecret);
}


public boolean processPar(AuthzRequest authzRequest) {
boolean isPar = Util.isPar(authzRequest.getRequestUri());
if (!isPar && isTrue(appConfiguration.getRequirePar())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ public ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredenti
return grant;
}

@Override
public TokenExchangeGrant createTokenExchangeGrant(User user, Client client) {
TokenExchangeGrant grant = grantInstance.select(TokenExchangeGrant.class).get();
grant.init(user, client);
return grant;
}

@Override
public CIBAGrant createCIBAGrant(CibaRequestCacheControl request) {
CIBAGrant grant = grantInstance.select(CIBAGrant.class).get();
Expand Down Expand Up @@ -308,6 +315,12 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) {

result = deviceCodeGrant;
break;
case TOKEN_EXCHANGE:
TokenExchangeGrant tokenExchangeGrant = grantInstance.select(TokenExchangeGrant.class).get();
tokenExchangeGrant.init(user, AuthorizationGrantType.TOKEN_EXCHANGE, client, tokenEntity.getCreationDate());

result = tokenExchangeGrant;
break;
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public enum AuthorizationGrantType implements HasParamName {
* grant types are not available (such as an authorization code).
*/
RESOURCE_OWNER_PASSWORD_CREDENTIALS("resource_owner_password_credentials"),

TOKEN_EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange"),
/**
* An extension grant for Client Initiated Backchannel Authentication.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public interface IAuthorizationGrantList {

ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredentialsGrant(User user, Client client);

TokenExchangeGrant createTokenExchangeGrant(User user, Client client);

CIBAGrant createCIBAGrant(CibaRequestCacheControl request);

AuthorizationCodeGrant getAuthorizationCodeGrant(String authorizationCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.jans.as.server.model.common;

import io.jans.as.common.model.common.User;
import io.jans.as.common.model.registration.Client;
import io.jans.as.model.common.GrantType;

/**
* @author Yuriy Z
*/
public class TokenExchangeGrant extends AuthorizationGrant {

public void init(User user, Client client) {
super.init(user, AuthorizationGrantType.TOKEN_EXCHANGE, client, null);
}

@Override
public GrantType getGrantType() {
return GrantType.TOKEN_EXCHANGE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ public class HandleTokenFactory {
public static String generateHandleToken() {
return UUID.randomUUID().toString();
}

public static String generateDeviceSecret() {
return UUID.randomUUID().toString();
}
}
Loading

0 comments on commit 595d1aa

Please sign in to comment.