001/******************************************************************************* 002 * Copyright 2017 The MIT Internet Trust Consortium 003 * 004 * Portions copyright 2011-2013 The MITRE Corporation 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 *******************************************************************************/ 018package org.mitre.openid.connect.token; 019 020import java.util.Date; 021import java.util.UUID; 022 023import org.mitre.jwt.signer.service.JWTSigningAndValidationService; 024import org.mitre.oauth2.model.ClientDetailsEntity; 025import org.mitre.oauth2.model.OAuth2AccessTokenEntity; 026import org.mitre.oauth2.service.ClientDetailsEntityService; 027import org.mitre.oauth2.service.SystemScopeService; 028import org.mitre.openid.connect.config.ConfigurationPropertiesBean; 029import org.mitre.openid.connect.model.UserInfo; 030import org.mitre.openid.connect.service.OIDCTokenService; 031import org.mitre.openid.connect.service.UserInfoService; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034import org.springframework.beans.factory.annotation.Autowired; 035import org.springframework.security.oauth2.common.OAuth2AccessToken; 036import org.springframework.security.oauth2.provider.OAuth2Authentication; 037import org.springframework.security.oauth2.provider.OAuth2Request; 038import org.springframework.security.oauth2.provider.token.TokenEnhancer; 039import org.springframework.stereotype.Service; 040 041import com.google.common.base.Strings; 042import com.google.common.collect.Lists; 043import com.nimbusds.jose.JWSAlgorithm; 044import com.nimbusds.jose.JWSHeader; 045import com.nimbusds.jwt.JWT; 046import com.nimbusds.jwt.JWTClaimsSet; 047import com.nimbusds.jwt.JWTClaimsSet.Builder; 048import com.nimbusds.jwt.SignedJWT; 049 050@Service 051public class ConnectTokenEnhancer implements TokenEnhancer { 052 053 /** 054 * Logger for this class 055 */ 056 private static final Logger logger = LoggerFactory.getLogger(ConnectTokenEnhancer.class); 057 058 @Autowired 059 private ConfigurationPropertiesBean configBean; 060 061 @Autowired 062 private JWTSigningAndValidationService jwtService; 063 064 @Autowired 065 private ClientDetailsEntityService clientService; 066 067 @Autowired 068 private UserInfoService userInfoService; 069 070 @Autowired 071 private OIDCTokenService connectTokenService; 072 073 @Override 074 public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 075 076 OAuth2AccessTokenEntity token = (OAuth2AccessTokenEntity) accessToken; 077 OAuth2Request originalAuthRequest = authentication.getOAuth2Request(); 078 079 String clientId = originalAuthRequest.getClientId(); 080 ClientDetailsEntity client = clientService.loadClientByClientId(clientId); 081 082 Builder builder = new JWTClaimsSet.Builder() 083 .claim("azp", clientId) 084 .issuer(configBean.getIssuer()) 085 .issueTime(new Date()) 086 .expirationTime(token.getExpiration()) 087 .subject(authentication.getName()) 088 .jwtID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it 089 090 String audience = (String) authentication.getOAuth2Request().getExtensions().get("aud"); 091 if (!Strings.isNullOrEmpty(audience)) { 092 builder.audience(Lists.newArrayList(audience)); 093 } 094 095 addCustomAccessTokenClaims(builder, token, authentication); 096 097 JWTClaimsSet claims = builder.build(); 098 099 JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm(); 100 JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null, 101 jwtService.getDefaultSignerKeyId(), 102 null, null); 103 SignedJWT signed = new SignedJWT(header, claims); 104 105 jwtService.signJwt(signed); 106 107 token.setJwt(signed); 108 109 /** 110 * Authorization request scope MUST include "openid" in OIDC, but access token request 111 * may or may not include the scope parameter. As long as the AuthorizationRequest 112 * has the proper scope, we can consider this a valid OpenID Connect request. Otherwise, 113 * we consider it to be a vanilla OAuth2 request. 114 * 115 * Also, there must be a user authentication involved in the request for it to be considered 116 * OIDC and not OAuth, so we check for that as well. 117 */ 118 if (originalAuthRequest.getScope().contains(SystemScopeService.OPENID_SCOPE) 119 && !authentication.isClientOnly()) { 120 121 String username = authentication.getName(); 122 UserInfo userInfo = userInfoService.getByUsernameAndClientId(username, clientId); 123 124 if (userInfo != null) { 125 126 JWT idToken = connectTokenService.createIdToken(client, 127 originalAuthRequest, claims.getIssueTime(), 128 userInfo.getSub(), token); 129 130 // attach the id token to the parent access token 131 token.setIdToken(idToken); 132 } else { 133 // can't create an id token if we can't find the user 134 logger.warn("Request for ID token when no user is present."); 135 } 136 } 137 138 return token; 139 } 140 141 public ConfigurationPropertiesBean getConfigBean() { 142 return configBean; 143 } 144 145 public void setConfigBean(ConfigurationPropertiesBean configBean) { 146 this.configBean = configBean; 147 } 148 149 public JWTSigningAndValidationService getJwtService() { 150 return jwtService; 151 } 152 153 public void setJwtService(JWTSigningAndValidationService jwtService) { 154 this.jwtService = jwtService; 155 } 156 157 public ClientDetailsEntityService getClientService() { 158 return clientService; 159 } 160 161 public void setClientService(ClientDetailsEntityService clientService) { 162 this.clientService = clientService; 163 } 164 165 166 /** 167 * Hook for subclasses that allows adding custom claims to the JWT that will be used as access token. 168 * @param builder the builder holding the current claims 169 * @param token the un-enhanced token 170 * @param authentication current authentication 171 */ 172 protected void addCustomAccessTokenClaims(JWTClaimsSet.Builder builder, OAuth2AccessTokenEntity token, 173 OAuth2Authentication authentication) { 174 } 175 176}