blob: 548d2276dfe0fbfb33e08d6b6786eb91c3961ce5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.syncope.core.logic;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.Attr;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.to.EntityTO;
import org.apache.syncope.common.lib.oidc.OIDCRequest;
import org.apache.syncope.common.lib.oidc.OIDCLoginResponse;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.CipherAlgorithm;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
import org.apache.syncope.core.logic.oidc.NoOpSessionStore;
import org.apache.syncope.core.logic.oidc.OIDC4UIContext;
import org.apache.syncope.core.logic.oidc.OIDCClientCache;
import org.apache.syncope.core.logic.oidc.OIDCUserManager;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.apache.syncope.core.spring.security.AuthDataAccessor;
import org.apache.syncope.core.spring.security.Encryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.apache.syncope.core.persistence.api.entity.OIDCC4UIProvider;
import org.apache.syncope.core.persistence.api.entity.OIDCC4UIProviderItem;
import org.apache.syncope.core.persistence.api.dao.OIDCC4UIProviderDAO;
import org.pac4j.core.exception.http.WithLocationAction;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.credentials.OidcCredentials;
import org.pac4j.oidc.profile.OidcProfile;
@Component
public class OIDCC4UILogic extends AbstractTransactionalLogic<EntityTO> {
private static final String JWT_CLAIM_OP_NAME = "OP_NAME";
private static final String JWT_CLAIM_ID_TOKEN = "ID_TOKEN";
private static final Encryptor ENCRYPTOR = Encryptor.getInstance();
@Autowired
private OIDCClientCache oidcClientClientCache;
@Autowired
private AuthDataAccessor authDataAccessor;
@Autowired
private AccessTokenDataBinder accessTokenDataBinder;
@Autowired
private OIDCC4UIProviderDAO opDAO;
@Autowired
private OIDCUserManager userManager;
private OidcClient getOidcClient(final OIDCC4UIProvider op, final String callbackUrl) {
return oidcClientClientCache.get(op.getName()).orElseGet(() -> oidcClientClientCache.add(op, callbackUrl));
}
private OidcClient getOidcClient(final String opName, final String callbackUrl) {
OIDCC4UIProvider op = opDAO.findByName(opName);
if (op == null) {
throw new NotFoundException("OIDC Provider '" + opName + '\'');
}
return getOidcClient(op, callbackUrl);
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
public OIDCRequest createLoginRequest(final String redirectURI, final String opName) {
// 1. look for OidcClient
OidcClient oidcClient = getOidcClient(opName, redirectURI);
oidcClient.setCallbackUrl(redirectURI);
// 2. create OIDCRequest
WithLocationAction action = oidcClient.getRedirectionAction(new OIDC4UIContext(), NoOpSessionStore.INSTANCE).
map(WithLocationAction.class::cast).
orElseThrow(() -> {
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add("No RedirectionAction generated for LoginRequest");
return sce;
});
OIDCRequest loginRequest = new OIDCRequest();
loginRequest.setLocation(action.getLocation());
return loginRequest;
}
@PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
public OIDCLoginResponse login(final String redirectURI, final String authorizationCode, final String opName) {
// 0. look for OP
OIDCC4UIProvider op = opDAO.findByName(opName);
if (op == null) {
throw new NotFoundException("OIDC Provider '" + opName + '\'');
}
// 1. look for configured client
OidcClient oidcClient = getOidcClient(opName, redirectURI);
oidcClient.setCallbackUrl(redirectURI);
// 2. get OpenID Connect tokens
String idTokenHint;
JWTClaimsSet idToken;
try {
OidcCredentials credentials = new OidcCredentials();
credentials.setCode(new AuthorizationCode(authorizationCode));
OIDC4UIContext ctx = new OIDC4UIContext();
oidcClient.getAuthenticator().validate(credentials, ctx, NoOpSessionStore.INSTANCE);
idToken = credentials.getIdToken().getJWTClaimsSet();
idTokenHint = credentials.getIdToken().serialize();
} catch (Exception e) {
LOG.error("While validating Token Response", e);
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add(e.getMessage());
throw sce;
}
// 3. prepare the result
OIDCLoginResponse loginResponse = new OIDCLoginResponse();
loginResponse.setLogoutSupported(StringUtils.isNotBlank(op.getEndSessionEndpoint()));
// 3a. find matching user (if any) and return the received attributes
String keyValue = idToken.getSubject();
for (OIDCC4UIProviderItem item : op.getItems()) {
Attr attrTO = new Attr();
attrTO.setSchema(item.getExtAttrName());
String value = idToken.getClaim(item.getExtAttrName()) == null
? null
: idToken.getClaim(item.getExtAttrName()).toString();
if (value != null) {
attrTO.getValues().add(value);
loginResponse.getAttrs().add(attrTO);
if (item.isConnObjectKey()) {
keyValue = value;
}
}
}
List<String> matchingUsers = keyValue == null
? List.of()
: userManager.findMatchingUser(keyValue, op.getConnObjectKeyItem().get());
LOG.debug("Found {} matching users for {}", matchingUsers.size(), keyValue);
// 3b. not found: create or selfreg if configured
String username;
if (matchingUsers.isEmpty()) {
if (op.isCreateUnmatching()) {
LOG.debug("No user matching {}, about to create", keyValue);
String defaultUsername = keyValue;
username = AuthContextUtils.callAsAdmin(AuthContextUtils.getDomain(),
() -> userManager.create(op, loginResponse, defaultUsername));
} else if (op.isSelfRegUnmatching()) {
UserTO userTO = new UserTO();
userManager.fill(op, loginResponse, userTO);
loginResponse.getAttrs().clear();
loginResponse.getAttrs().addAll(userTO.getPlainAttrs());
if (StringUtils.isNotBlank(userTO.getUsername())) {
loginResponse.setUsername(userTO.getUsername());
} else {
loginResponse.setUsername(keyValue);
}
loginResponse.setSelfReg(true);
return loginResponse;
} else {
throw new NotFoundException(Optional.ofNullable(keyValue).
map(value -> "User matching the provided value " + value).
orElse("User marching the provided claims"));
}
} else if (matchingUsers.size() > 1) {
throw new IllegalArgumentException("Several users match the provided value " + keyValue);
} else {
if (op.isUpdateMatching()) {
LOG.debug("About to update {} for {}", matchingUsers.get(0), keyValue);
username = AuthContextUtils.callAsAdmin(AuthContextUtils.getDomain(),
() -> userManager.update(matchingUsers.get(0), op, loginResponse));
} else {
username = matchingUsers.get(0);
}
}
loginResponse.setUsername(username);
// 4. generate JWT for further access
Map<String, Object> claims = new HashMap<>();
claims.put(JWT_CLAIM_OP_NAME, opName);
claims.put(JWT_CLAIM_ID_TOKEN, idTokenHint);
byte[] authorities = null;
try {
authorities = ENCRYPTOR.encode(POJOHelper.serialize(
authDataAccessor.getAuthorities(loginResponse.getUsername(), null)), CipherAlgorithm.AES).
getBytes();
} catch (Exception e) {
LOG.error("Could not fetch authorities", e);
}
Pair<String, Date> accessTokenInfo =
accessTokenDataBinder.create(loginResponse.getUsername(), claims, authorities, true);
loginResponse.setAccessToken(accessTokenInfo.getLeft());
loginResponse.setAccessTokenExpiryTime(accessTokenInfo.getRight());
return loginResponse;
}
@PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "'))")
public OIDCRequest createLogoutRequest(final String accessToken, final String redirectURI) {
// 0. fetch the current JWT used for Syncope authentication
JWTClaimsSet claimsSet;
try {
SignedJWT jwt = SignedJWT.parse(accessToken);
claimsSet = jwt.getJWTClaimsSet();
} catch (ParseException e) {
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidAccessToken);
sce.getElements().add(e.getMessage());
throw sce;
}
// 1. look for OidcClient
OidcClient oidcClient =
getOidcClient((String) claimsSet.getClaim(JWT_CLAIM_OP_NAME), redirectURI);
oidcClient.setCallbackUrl(redirectURI);
// 2. create OIDCRequest
OidcProfile profile = new OidcProfile();
profile.setIdTokenString((String) claimsSet.getClaim(JWT_CLAIM_ID_TOKEN));
WithLocationAction action = oidcClient.getLogoutAction(
new OIDC4UIContext(),
NoOpSessionStore.INSTANCE,
profile,
redirectURI).
map(WithLocationAction.class::cast).
orElseThrow(() -> {
SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
sce.getElements().add("No RedirectionAction generated for LogoutRequest");
return sce;
});
OIDCRequest logoutRequest = new OIDCRequest();
logoutRequest.setLocation(action.getLocation());
return logoutRequest;
}
@Override
protected EntityTO resolveReference(
final Method method, final Object... args) throws UnresolvedReferenceException {
throw new UnresolvedReferenceException();
}
}