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.service.impl;
019
020import static org.mitre.openid.connect.request.ConnectRequestParameters.MAX_AGE;
021import static org.mitre.openid.connect.request.ConnectRequestParameters.NONCE;
022
023import java.util.Date;
024import java.util.Map;
025import java.util.Set;
026import java.util.UUID;
027
028import org.mitre.jwt.encryption.service.JWTEncryptionAndDecryptionService;
029import org.mitre.jwt.signer.service.JWTSigningAndValidationService;
030import org.mitre.jwt.signer.service.impl.ClientKeyCacheService;
031import org.mitre.jwt.signer.service.impl.SymmetricKeyJWTValidatorCacheService;
032import org.mitre.oauth2.model.AuthenticationHolderEntity;
033import org.mitre.oauth2.model.ClientDetailsEntity;
034import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
035import org.mitre.oauth2.repository.AuthenticationHolderRepository;
036import org.mitre.oauth2.service.OAuth2TokenEntityService;
037import org.mitre.oauth2.service.SystemScopeService;
038import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
039import org.mitre.openid.connect.service.OIDCTokenService;
040import org.mitre.openid.connect.util.IdTokenHashUtils;
041import org.mitre.openid.connect.web.AuthenticationTimeStamper;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044import org.springframework.beans.factory.annotation.Autowired;
045import org.springframework.security.core.AuthenticationException;
046import org.springframework.security.core.authority.SimpleGrantedAuthority;
047import org.springframework.security.oauth2.provider.OAuth2Authentication;
048import org.springframework.security.oauth2.provider.OAuth2Request;
049import org.springframework.stereotype.Service;
050
051import com.google.common.base.Strings;
052import com.google.common.collect.Lists;
053import com.google.common.collect.Maps;
054import com.google.common.collect.Sets;
055import com.nimbusds.jose.Algorithm;
056import com.nimbusds.jose.JWEHeader;
057import com.nimbusds.jose.JWEObject;
058import com.nimbusds.jose.JWSAlgorithm;
059import com.nimbusds.jose.JWSHeader;
060import com.nimbusds.jose.util.Base64URL;
061import com.nimbusds.jwt.EncryptedJWT;
062import com.nimbusds.jwt.JWT;
063import com.nimbusds.jwt.JWTClaimsSet;
064import com.nimbusds.jwt.PlainJWT;
065import com.nimbusds.jwt.SignedJWT;
066/**
067 * Default implementation of service to create specialty OpenID Connect tokens.
068 *
069 * @author Amanda Anganes
070 *
071 */
072@Service
073public class DefaultOIDCTokenService implements OIDCTokenService {
074
075        /**
076         * Logger for this class
077         */
078        private static final Logger logger = LoggerFactory.getLogger(DefaultOIDCTokenService.class);
079
080        @Autowired
081        private JWTSigningAndValidationService jwtService;
082
083        @Autowired
084        private AuthenticationHolderRepository authenticationHolderRepository;
085
086        @Autowired
087        private ConfigurationPropertiesBean configBean;
088
089        @Autowired
090        private ClientKeyCacheService encrypters;
091
092        @Autowired
093        private SymmetricKeyJWTValidatorCacheService symmetricCacheService;
094
095        @Autowired
096        private OAuth2TokenEntityService tokenService;
097
098        @Override
099        public JWT createIdToken(ClientDetailsEntity client, OAuth2Request request, Date issueTime, String sub, OAuth2AccessTokenEntity accessToken) {
100
101                JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm();
102
103                if (client.getIdTokenSignedResponseAlg() != null) {
104                        signingAlg = client.getIdTokenSignedResponseAlg();
105                }
106
107
108                JWT idToken = null;
109
110                JWTClaimsSet.Builder idClaims = new JWTClaimsSet.Builder();
111
112                // if the auth time claim was explicitly requested OR if the client always wants the auth time, put it in
113                if (request.getExtensions().containsKey(MAX_AGE)
114                                || (request.getExtensions().containsKey("idtoken")) // TODO: parse the ID Token claims (#473) -- for now assume it could be in there
115                                || (client.getRequireAuthTime() != null && client.getRequireAuthTime())) {
116
117                        if (request.getExtensions().get(AuthenticationTimeStamper.AUTH_TIMESTAMP) != null) {
118
119                                Long authTimestamp = Long.parseLong((String) request.getExtensions().get(AuthenticationTimeStamper.AUTH_TIMESTAMP));
120                                if (authTimestamp != null) {
121                                        idClaims.claim("auth_time", authTimestamp / 1000L);
122                                }
123                        } else {
124                                // we couldn't find the timestamp!
125                                logger.warn("Unable to find authentication timestamp! There is likely something wrong with the configuration.");
126                        }
127                }
128
129                idClaims.issueTime(issueTime);
130
131                if (client.getIdTokenValiditySeconds() != null) {
132                        Date expiration = new Date(System.currentTimeMillis() + (client.getIdTokenValiditySeconds() * 1000L));
133                        idClaims.expirationTime(expiration);
134                }
135
136                idClaims.issuer(configBean.getIssuer());
137                idClaims.subject(sub);
138                idClaims.audience(Lists.newArrayList(client.getClientId()));
139                idClaims.jwtID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it
140
141                String nonce = (String)request.getExtensions().get(NONCE);
142                if (!Strings.isNullOrEmpty(nonce)) {
143                        idClaims.claim("nonce", nonce);
144                }
145
146                Set<String> responseTypes = request.getResponseTypes();
147
148                if (responseTypes.contains("token")) {
149                        // calculate the token hash
150                        Base64URL at_hash = IdTokenHashUtils.getAccessTokenHash(signingAlg, accessToken);
151                        idClaims.claim("at_hash", at_hash);
152                }
153
154                addCustomIdTokenClaims(idClaims, client, request, sub, accessToken);
155
156                if (client.getIdTokenEncryptedResponseAlg() != null && !client.getIdTokenEncryptedResponseAlg().equals(Algorithm.NONE)
157                                && client.getIdTokenEncryptedResponseEnc() != null && !client.getIdTokenEncryptedResponseEnc().equals(Algorithm.NONE)
158                                && (!Strings.isNullOrEmpty(client.getJwksUri()) || client.getJwks() != null)) {
159
160                        JWTEncryptionAndDecryptionService encrypter = encrypters.getEncrypter(client);
161
162                        if (encrypter != null) {
163
164                                idToken = new EncryptedJWT(new JWEHeader(client.getIdTokenEncryptedResponseAlg(), client.getIdTokenEncryptedResponseEnc()), idClaims.build());
165
166                                encrypter.encryptJwt((JWEObject) idToken);
167
168                        } else {
169                                logger.error("Couldn't find encrypter for client: " + client.getClientId());
170                        }
171
172                } else {
173
174                        if (signingAlg.equals(Algorithm.NONE)) {
175                                // unsigned ID token
176                                idToken = new PlainJWT(idClaims.build());
177
178                        } else {
179
180                                // signed ID token
181
182                                if (signingAlg.equals(JWSAlgorithm.HS256)
183                                                || signingAlg.equals(JWSAlgorithm.HS384)
184                                                || signingAlg.equals(JWSAlgorithm.HS512)) {
185
186                                        JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null,
187                                                        jwtService.getDefaultSignerKeyId(),
188                                                        null, null);
189                                        idToken = new SignedJWT(header, idClaims.build());
190
191                                        JWTSigningAndValidationService signer = symmetricCacheService.getSymmetricValidtor(client);
192
193                                        // sign it with the client's secret
194                                        signer.signJwt((SignedJWT) idToken);
195                                } else {
196                                        idClaims.claim("kid", jwtService.getDefaultSignerKeyId());
197
198                                        JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null,
199                                                        jwtService.getDefaultSignerKeyId(),
200                                                        null, null);
201
202                                        idToken = new SignedJWT(header, idClaims.build());
203
204                                        // sign it with the server's key
205                                        jwtService.signJwt((SignedJWT) idToken);
206                                }
207                        }
208
209                }
210
211                return idToken;
212        }
213
214        /**
215         * @param client
216         * @return
217         * @throws AuthenticationException
218         */
219        @Override
220        public OAuth2AccessTokenEntity createRegistrationAccessToken(ClientDetailsEntity client) {
221
222                return createAssociatedToken(client, Sets.newHashSet(SystemScopeService.REGISTRATION_TOKEN_SCOPE));
223
224        }
225
226        /**
227         * @param client
228         * @return
229         */
230        @Override
231        public OAuth2AccessTokenEntity createResourceAccessToken(ClientDetailsEntity client) {
232
233                return createAssociatedToken(client, Sets.newHashSet(SystemScopeService.RESOURCE_TOKEN_SCOPE));
234
235        }
236
237        @Override
238        public OAuth2AccessTokenEntity rotateRegistrationAccessTokenForClient(ClientDetailsEntity client) {
239                // revoke any previous tokens
240                OAuth2AccessTokenEntity oldToken = tokenService.getRegistrationAccessTokenForClient(client);
241                if (oldToken != null) {
242                        Set<String> scope = oldToken.getScope();
243                        tokenService.revokeAccessToken(oldToken);
244                        return createAssociatedToken(client, scope);
245                } else {
246                        return null;
247                }
248
249        }
250
251        private OAuth2AccessTokenEntity createAssociatedToken(ClientDetailsEntity client, Set<String> scope) {
252
253                // revoke any previous tokens that might exist, just to be sure
254                OAuth2AccessTokenEntity oldToken = tokenService.getRegistrationAccessTokenForClient(client);
255                if (oldToken != null) {
256                        tokenService.revokeAccessToken(oldToken);
257                }
258
259                // create a new token
260
261                Map<String, String> authorizationParameters = Maps.newHashMap();
262                OAuth2Request clientAuth = new OAuth2Request(authorizationParameters, client.getClientId(),
263                                Sets.newHashSet(new SimpleGrantedAuthority("ROLE_CLIENT")), true,
264                                scope, null, null, null, null);
265                OAuth2Authentication authentication = new OAuth2Authentication(clientAuth, null);
266
267                OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity();
268                token.setClient(client);
269                token.setScope(scope);
270
271                AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity();
272                authHolder.setAuthentication(authentication);
273                authHolder = authenticationHolderRepository.save(authHolder);
274                token.setAuthenticationHolder(authHolder);
275
276                JWTClaimsSet claims = new JWTClaimsSet.Builder()
277                                .audience(Lists.newArrayList(client.getClientId()))
278                                .issuer(configBean.getIssuer())
279                                .issueTime(new Date())
280                                .expirationTime(token.getExpiration())
281                                .jwtID(UUID.randomUUID().toString()) // set a random NONCE in the middle of it
282                                .build();
283
284                JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm();
285                JWSHeader header = new JWSHeader(signingAlg, null, null, null, null, null, null, null, null, null,
286                                jwtService.getDefaultSignerKeyId(),
287                                null, null);
288                SignedJWT signed = new SignedJWT(header, claims);
289
290                jwtService.signJwt(signed);
291
292                token.setJwt(signed);
293
294                return token;
295        }
296
297        /**
298         * @return the configBean
299         */
300        public ConfigurationPropertiesBean getConfigBean() {
301                return configBean;
302        }
303
304        /**
305         * @param configBean the configBean to set
306         */
307        public void setConfigBean(ConfigurationPropertiesBean configBean) {
308                this.configBean = configBean;
309        }
310
311        /**
312         * @return the jwtService
313         */
314        public JWTSigningAndValidationService getJwtService() {
315                return jwtService;
316        }
317
318        /**
319         * @param jwtService the jwtService to set
320         */
321        public void setJwtService(JWTSigningAndValidationService jwtService) {
322                this.jwtService = jwtService;
323        }
324
325        /**
326         * @return the authenticationHolderRepository
327         */
328        public AuthenticationHolderRepository getAuthenticationHolderRepository() {
329                return authenticationHolderRepository;
330        }
331
332        /**
333         * @param authenticationHolderRepository the authenticationHolderRepository to set
334         */
335        public void setAuthenticationHolderRepository(
336                        AuthenticationHolderRepository authenticationHolderRepository) {
337                this.authenticationHolderRepository = authenticationHolderRepository;
338        }
339
340        /**
341         * Hook for subclasses that allows adding custom claims to the JWT
342         * that will be used as id token.
343         * @param idClaims the builder holding the current claims
344         * @param client information about the requesting client
345         * @param request request that caused the id token to be created
346         * @param sub subject auf the id token
347         * @param accessToken the access token
348         * @param authentication current authentication
349         */
350        protected void addCustomIdTokenClaims(JWTClaimsSet.Builder idClaims, ClientDetailsEntity client, OAuth2Request request,
351            String sub, OAuth2AccessTokenEntity accessToken) {
352        }
353
354}