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}