DefaultOIDCTokenService.java
/*******************************************************************************
* Copyright 2017 The MIT Internet Trust Consortium
*
* Portions copyright 2011-2013 The MITRE Corporation
*
* 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.mitre.openid.connect.service.impl;
import static org.mitre.openid.connect.request.ConnectRequestParameters.MAX_AGE;
import static org.mitre.openid.connect.request.ConnectRequestParameters.NONCE;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService;
import org.mitre.jwt.signer.service.JWTSigningAndValidationService;
import org.mitre.jwt.signer.service.impl.ClientKeyCacheService;
import org.mitre.jwt.signer.service.impl.SymmetricKeyJWTValidatorCacheService;
import org.mitre.oauth2.model.AuthenticationHolderEntity;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.repository.AuthenticationHolderRepository;
import org.mitre.oauth2.service.OAuth2TokenEntityService;
import org.mitre.oauth2.service.SystemScopeService;
import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
import org.mitre.openid.connect.service.OIDCTokenService;
import org.mitre.openid.connect.util.IdTokenHashUtils;
import org.mitre.openid.connect.web.AuthenticationTimeStamper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.stereotype.Service;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.SignedJWT;
/**
* Default implementation of service to create specialty OpenID Connect tokens.
*
* @author Amanda Anganes
*
*/
@Service
public class DefaultOIDCTokenService implements OIDCTokenService {
/**
* Logger for this class
*/
private static final Logger logger = LoggerFactory.getLogger(DefaultOIDCTokenService.class);
@Autowired
private JWTSigningAndValidationService jwtService;
@Autowired
private AuthenticationHolderRepository authenticationHolderRepository;
@Autowired
private ConfigurationPropertiesBean configBean;
@Autowired
private ClientKeyCacheService encrypters;
@Autowired
private SymmetricKeyJWTValidatorCacheService symmetricCacheService;
@Autowired
private OAuth2TokenEntityService tokenService;
@Override
public JWT createIdToken(ClientDetailsEntity client, OAuth2Request request, Date issueTime, String sub, OAuth2AccessTokenEntity accessToken) {
JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm();
if (client.getIdTokenSignedResponseAlg() != null) {
signingAlg = client.getIdTokenSignedResponseAlg();
}
JWT idToken = null;
JWTClaimsSet.Builder idClaims = new JWTClaimsSet.Builder();
// if the auth time claim was explicitly requested OR if the client always wants the auth time, put it in
if (request.getExtensions().containsKey(MAX_AGE)
|| (request.getExtensions().containsKey("idtoken")) // TODO: parse the ID Token claims (#473) -- for now assume it could be in there
|| (client.getRequireAuthTime() != null && client.getRequireAuthTime())) {
if (request.getExtensions().get(AuthenticationTimeStamper.AUTH_TIMESTAMP) != null) {
Long authTimestamp = Long.parseLong((String) request.getExtensions().get(AuthenticationTimeStamper.AUTH_TIMESTAMP));
if (authTimestamp != null) {
idClaims.claim("auth_time", authTimestamp / 1000L);
}
} else {
// we couldn't find the timestamp!
logger.warn("Unable to find authentication timestamp! There is likely something wrong with the configuration.");
}
}
idClaims.issueTime(issueTime);
if (client.getIdTokenValiditySeconds() != null) {
Date expiration = new Date(System.currentTimeMillis() + (client.getIdTokenValiditySeconds() * 1000L));
idClaims.expirationTime(expiration);
}
idClaims.issuer(configBean.getIssuer());
idClaims.subject(sub);
idClaims.audience(Lists.newArrayList(client.getClientId()));
idClaims.jwtID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it
String nonce = (String)request.getExtensions().get(NONCE);
if (!Strings.isNullOrEmpty(nonce)) {
idClaims.claim("nonce", nonce);
}
Set<String> responseTypes = request.getResponseTypes();
if (responseTypes.contains("token")) {
// calculate the token hash
Base64URL at_hash = IdTokenHashUtils.getAccessTokenHash(signingAlg, accessToken);
idClaims.claim("at_hash", at_hash);
}
addCustomIdTokenClaims(idClaims, client, request, sub, accessToken);
if (client.getIdTokenEncryptedResponseAlg() != null && !client.getIdTokenEncryptedResponseAlg().equals(Algorithm.NONE)
&& client.getIdTokenEncryptedResponseEnc() != null && !client.getIdTokenEncryptedResponseEnc().equals(Algorithm.NONE)
&& (!Strings.isNullOrEmpty(client.getJwksUri()) || client.getJwks() != null)) {
JWTEncryptionAndDecryptionService encrypter = encrypters.getEncrypter(client);
if (encrypter != null) {
idToken = new EncryptedJWT(new JWEHeader(client.getIdTokenEncryptedResponseAlg(), client.getIdTokenEncryptedResponseEnc()), idClaims.build());
encrypter.encryptJwt((JWEObject) idToken);
} else {
logger.error("Couldn't find encrypter for client: " + client.getClientId());
}
} else {
if (signingAlg.equals(Algorithm.NONE)) {
// unsigned ID token
idToken = new PlainJWT(idClaims.build());
} else {
// signed ID token
if (signingAlg.equals(JWSAlgorithm.HS256)
|| signingAlg.equals(JWSAlgorithm.HS384)
|| signingAlg.equals(JWSAlgorithm.HS512)) {
JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null,
jwtService.getDefaultSignerKeyId(),
null, null);
idToken = new SignedJWT(header, idClaims.build());
JWTSigningAndValidationService signer = symmetricCacheService.getSymmetricValidtor(client);
// sign it with the client's secret
signer.signJwt((SignedJWT) idToken);
} else {
idClaims.claim("kid", jwtService.getDefaultSignerKeyId());
JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null,
jwtService.getDefaultSignerKeyId(),
null, null);
idToken = new SignedJWT(header, idClaims.build());
// sign it with the server's key
jwtService.signJwt((SignedJWT) idToken);
}
}
}
return idToken;
}
/**
* @param client
* @return
* @throws AuthenticationException
*/
@Override
public OAuth2AccessTokenEntity createRegistrationAccessToken(ClientDetailsEntity client) {
return createAssociatedToken(client, Sets.newHashSet(SystemScopeService.REGISTRATION_TOKEN_SCOPE));
}
/**
* @param client
* @return
*/
@Override
public OAuth2AccessTokenEntity createResourceAccessToken(ClientDetailsEntity client) {
return createAssociatedToken(client, Sets.newHashSet(SystemScopeService.RESOURCE_TOKEN_SCOPE));
}
@Override
public OAuth2AccessTokenEntity rotateRegistrationAccessTokenForClient(ClientDetailsEntity client) {
// revoke any previous tokens
OAuth2AccessTokenEntity oldToken = tokenService.getRegistrationAccessTokenForClient(client);
if (oldToken != null) {
Set<String> scope = oldToken.getScope();
tokenService.revokeAccessToken(oldToken);
return createAssociatedToken(client, scope);
} else {
return null;
}
}
private OAuth2AccessTokenEntity createAssociatedToken(ClientDetailsEntity client, Set<String> scope) {
// revoke any previous tokens that might exist, just to be sure
OAuth2AccessTokenEntity oldToken = tokenService.getRegistrationAccessTokenForClient(client);
if (oldToken != null) {
tokenService.revokeAccessToken(oldToken);
}
// create a new token
Map<String, String> authorizationParameters = Maps.newHashMap();
OAuth2Request clientAuth = new OAuth2Request(authorizationParameters, client.getClientId(),
Sets.newHashSet(new SimpleGrantedAuthority("ROLE_CLIENT")), true,
scope, null, null, null, null);
OAuth2Authentication authentication = new OAuth2Authentication(clientAuth, null);
OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity();
token.setClient(client);
token.setScope(scope);
AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity();
authHolder.setAuthentication(authentication);
authHolder = authenticationHolderRepository.save(authHolder);
token.setAuthenticationHolder(authHolder);
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.audience(Lists.newArrayList(client.getClientId()))
.issuer(configBean.getIssuer())
.issueTime(new Date())
.expirationTime(token.getExpiration())
.jwtID(UUID.randomUUID().toString()) // set a random NONCE in the middle of it
.build();
JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm();
JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null,
jwtService.getDefaultSignerKeyId(),
null, null);
SignedJWT signed = new SignedJWT(header, claims);
jwtService.signJwt(signed);
token.setJwt(signed);
return token;
}
/**
* @return the configBean
*/
public ConfigurationPropertiesBean getConfigBean() {
return configBean;
}
/**
* @param configBean the configBean to set
*/
public void setConfigBean(ConfigurationPropertiesBean configBean) {
this.configBean = configBean;
}
/**
* @return the jwtService
*/
public JWTSigningAndValidationService getJwtService() {
return jwtService;
}
/**
* @param jwtService the jwtService to set
*/
public void setJwtService(JWTSigningAndValidationService jwtService) {
this.jwtService = jwtService;
}
/**
* @return the authenticationHolderRepository
*/
public AuthenticationHolderRepository getAuthenticationHolderRepository() {
return authenticationHolderRepository;
}
/**
* @param authenticationHolderRepository the authenticationHolderRepository to set
*/
public void setAuthenticationHolderRepository(
AuthenticationHolderRepository authenticationHolderRepository) {
this.authenticationHolderRepository = authenticationHolderRepository;
}
/**
* Hook for subclasses that allows adding custom claims to the JWT
* that will be used as id token.
* @param idClaims the builder holding the current claims
* @param client information about the requesting client
* @param request request that caused the id token to be created
* @param sub subject auf the id token
* @param accessToken the access token
* @param authentication current authentication
*/
protected void addCustomIdTokenClaims(JWTClaimsSet.Builder idClaims, ClientDetailsEntity client, OAuth2Request request,
String sub, OAuth2AccessTokenEntity accessToken) {
}
}