3939import java .util .Collections ;
4040import java .util .Date ;
4141import java .util .HashMap ;
42+ import java .util .HashSet ;
4243import java .util .LinkedHashMap ;
4344import java .util .Map ;
45+ import java .util .Set ;
4446import java .util .UUID ;
4547
4648/**
47- * Run with "mvn spring-boot:run" .
48- * <p>
49- * Provides OIDC metadata. See the spec at https://openid.net/specs/openid-connect-discovery-1_0.html
49+ * Implementation of all necessary OIDC endpoints .
50+ *
51+ * @author Martin Kuba [email protected] 5052 */
5153@ RestController
5254public class OidcController {
@@ -64,10 +66,13 @@ public class OidcController {
6466 private JWKSet publicJWKSet ;
6567 private JWSHeader jwsHeader ;
6668
67- private Map <String , AccessTokenInfo > accessTokens = new HashMap <>();
69+ private final Map <String , AccessTokenInfo > accessTokens = new HashMap <>();
6870
69- @ Autowired
70- private FakeOidcServerProperties serverProperties ;
71+ private final FakeOidcServerProperties serverProperties ;
72+
73+ public OidcController (@ Autowired FakeOidcServerProperties serverProperties ) {
74+ this .serverProperties = serverProperties ;
75+ }
7176
7277 @ PostConstruct
7378 public void init () throws IOException , ParseException , JOSEException {
@@ -80,6 +85,9 @@ public void init() throws IOException, ParseException, JOSEException {
8085 log .info ("config {}" , serverProperties );
8186 }
8287
88+ /**
89+ * Provides OIDC metadata. See the spec at https://openid.net/specs/openid-connect-discovery-1_0.html
90+ */
8391 @ RequestMapping (value = METADATA_ENDPOINT , method = RequestMethod .GET , produces = MediaType .APPLICATION_JSON_VALUE )
8492 @ CrossOrigin
8593 public ResponseEntity <?> metadata (UriComponentsBuilder uriBuilder , HttpServletRequest req ) {
@@ -101,13 +109,19 @@ public ResponseEntity<?> metadata(UriComponentsBuilder uriBuilder, HttpServletRe
101109 return ResponseEntity .ok ().body (m );
102110 }
103111
112+ /**
113+ * Provides JSON Web Key Set containing the public part of the key used to sign ID tokens.
114+ */
104115 @ RequestMapping (value = JWKS_ENDPOINT , method = RequestMethod .GET , produces = MediaType .APPLICATION_JSON_VALUE )
105116 @ CrossOrigin
106117 public ResponseEntity <String > jwks (HttpServletRequest req ) {
107118 log .info ("called " + JWKS_ENDPOINT + " from {}" , req .getRemoteHost ());
108119 return ResponseEntity .ok ().body (publicJWKSet .toString ());
109120 }
110121
122+ /**
123+ * Provides claims about a user. Requires a valid access token.
124+ */
111125 @ RequestMapping (value = USERINFO_ENDPOINT , method = RequestMethod .GET , produces = MediaType .APPLICATION_JSON_VALUE )
112126 @ CrossOrigin (allowedHeaders = {"Authorization" , "Content-Type" })
113127 public ResponseEntity <?> userinfo (@ RequestHeader ("Authorization" ) String auth , HttpServletRequest req ) {
@@ -120,41 +134,53 @@ public ResponseEntity<?> userinfo(@RequestHeader("Authorization") String auth, H
120134 if (accessTokenInfo == null ) {
121135 return ResponseEntity .status (HttpStatus .UNAUTHORIZED ).body ("access token not found" );
122136 }
137+ Set <String > scopes = new HashSet <>(Arrays .asList (accessTokenInfo .scope .split (" " )));
123138 Map <String , Object > m = new LinkedHashMap <>();
124139 User user = accessTokenInfo .user ;
125140 m .put ("sub" , user .getSub ());
126- m .put ("name" , user .getName ());
127- m .put ("family_name" , user .getFamily_name ());
128- m .put ("given_name" , user .getGiven_name ());
129- m .put ("preferred_username" , user .getPreferred_username ());
130- m .put ("email" , user .getEmail ());
141+ if (scopes .contains ("profile" )) {
142+ m .put ("name" , user .getName ());
143+ m .put ("family_name" , user .getFamily_name ());
144+ m .put ("given_name" , user .getGiven_name ());
145+ m .put ("preferred_username" , user .getPreferred_username ());
146+ }
147+ if (scopes .contains ("email" )) {
148+ m .put ("email" , user .getEmail ());
149+ }
131150 return ResponseEntity .ok ().body (m );
132151 }
133152
153+ /**
154+ * Provides information about a supplied access token.
155+ */
134156 @ RequestMapping (value = INTROSPECTION_ENDPOINT , method = RequestMethod .POST , produces = MediaType .APPLICATION_JSON_VALUE )
135157 public ResponseEntity <?> introspection (@ RequestParam String token ,
136158 @ RequestHeader ("Authorization" ) String auth ,
137- UriComponentsBuilder uriBuilder ,
138159 HttpServletRequest req ) {
139160 log .info ("called " + INTROSPECTION_ENDPOINT + " from {}" , req .getRemoteHost ());
140161 Map <String , Object > m = new LinkedHashMap <>();
141162 AccessTokenInfo accessTokenInfo = accessTokens .get (token );
142- if ( accessTokenInfo == null ) {
163+ if ( accessTokenInfo == null ) {
143164 log .error ("token not found in memory: {}" , token );
144165 m .put ("active" , false );
145166 } else {
146- String scopes = String .join (" " , accessTokenInfo .scopes );
147- log .info ("token found, releasing scopes: {}" , scopes );
148- m .put ("iss" , uriBuilder .replacePath (null ).build ().encode ().toUriString () + "/" );
167+ log .info ("found token for user {}, releasing scopes: {}" , accessTokenInfo .user .getSub (), accessTokenInfo .scope );
168+ // see https://tools.ietf.org/html/rfc7662#section-2.2 for all claims
149169 m .put ("active" , true );
150- m .put ("scope" , scopes );
170+ m .put ("scope" , accessTokenInfo .scope );
171+ m .put ("client_id" , accessTokenInfo .clientId );
151172 m .put ("username" , accessTokenInfo .user .getSub ());
152- m .put ("sub " , accessTokenInfo . user . getSub () );
173+ m .put ("token_type " , "Bearer" );
153174 m .put ("exp" , accessTokenInfo .expiration .toInstant ().toEpochMilli ());
175+ m .put ("sub" , accessTokenInfo .user .getSub ());
176+ m .put ("iss" , accessTokenInfo .iss );
154177 }
155178 return ResponseEntity .ok ().body (m );
156179 }
157180
181+ /**
182+ * Provides authorization endpoint.
183+ */
158184 @ RequestMapping (value = AUTHORIZATION_ENDPOINT , method = RequestMethod .GET )
159185 public ResponseEntity <?> authorize (@ RequestParam String client_id ,
160186 @ RequestParam String redirect_uri ,
@@ -165,26 +191,26 @@ public ResponseEntity<?> authorize(@RequestParam String client_id,
165191 @ RequestHeader (name = "Authorization" , required = false ) String auth ,
166192 UriComponentsBuilder uriBuilder ,
167193 HttpServletRequest req ) throws JOSEException , NoSuchAlgorithmException {
168- log .info ("called " + AUTHORIZATION_ENDPOINT + " from {}, scope={} response_type={} client_id={} redirect_uri={}" ,
194+ log .info ("called " + AUTHORIZATION_ENDPOINT + " from {}, scope={} response_type={} client_id={} redirect_uri={}" ,
169195 req .getRemoteHost (), scope , response_type , client_id , redirect_uri );
170196 if (auth == null ) {
171197 log .info ("user and password not provided" );
172198 return response401 ();
173199 } else {
174200 String [] creds = new String (Base64 .getDecoder ().decode (auth .split (" " )[1 ])).split (":" , 2 );
175- String logname = creds [0 ];
201+ String login = creds [0 ];
176202 String password = creds [1 ];
177203 User user = serverProperties .getUser ();
178- if (user .getLogname ().equals (logname ) && user .getPassword ().equals (password )) {
179- log .info ("password for user {} is correct" , logname );
204+ if (user .getLogname ().equals (login ) && user .getPassword ().equals (password )) {
205+ log .info ("password for user {} is correct" , login );
180206 String iss = uriBuilder .replacePath ("/" ).build ().encode ().toUriString ();
181207 String access_token = createAccessToken (iss , user , client_id , scope );
182208 String id_token = createIdToken (iss , user , client_id , nonce , access_token );
183209 String url = redirect_uri + "#" +
184210 "access_token=" + urlencode (access_token ) +
185211 "&token_type=Bearer" +
186212 "&state=" + urlencode (state ) +
187- "&expires_in=36000" +
213+ "&expires_in=" + serverProperties . getTokenExpirationSeconds () +
188214 "&id_token=" + urlencode (id_token );
189215 return ResponseEntity .status (HttpStatus .FOUND ).header ("Location" , url ).build ();
190216 } else {
@@ -211,16 +237,16 @@ private String createAccessToken(String iss, User user, String client_id, String
211237 // sign the JWT token
212238 jwt .sign (signer );
213239 String access_token = jwt .serialize ();
214- accessTokens .put (access_token , new AccessTokenInfo (user , access_token , expiration , scope . split ( " " ) ));
240+ accessTokens .put (access_token , new AccessTokenInfo (user , access_token , expiration , scope , client_id , iss ));
215241 return access_token ;
216242 }
217243
218244 private String createIdToken (String iss , User user , String client_id , String nonce , String accessToken ) throws NoSuchAlgorithmException , JOSEException {
219245 // compute at_hash
220- MessageDigest hasher = MessageDigest .getInstance ("SHA-256" );
221- hasher .reset ();
222- hasher .update (accessToken .getBytes (StandardCharsets .UTF_8 ));
223- byte [] hashBytes = hasher .digest ();
246+ MessageDigest digest = MessageDigest .getInstance ("SHA-256" );
247+ digest .reset ();
248+ digest .update (accessToken .getBytes (StandardCharsets .UTF_8 ));
249+ byte [] hashBytes = digest .digest ();
224250 byte [] hashBytesLeftHalf = Arrays .copyOf (hashBytes , hashBytes .length / 2 );
225251 Base64URL encodedHash = Base64URL .encode (hashBytesLeftHalf );
226252 // create JWT claims
@@ -254,32 +280,21 @@ private static ResponseEntity<String> response401() {
254280
255281
256282 private static class AccessTokenInfo {
257- User user ;
258- String accessToken ;
259- Date expiration ;
260- String [] scopes ;
283+ final User user ;
284+ final String accessToken ;
285+ final Date expiration ;
286+ final String scope ;
287+ final String clientId ;
288+ final String iss ;
261289
262- public AccessTokenInfo (User user , String accessToken , Date expiration , String [] scopes ) {
290+ public AccessTokenInfo (User user , String accessToken , Date expiration , String scope , String clientId , String iss ) {
263291 this .user = user ;
264292 this .accessToken = accessToken ;
265293 this .expiration = expiration ;
266- this .scopes = scopes ;
294+ this .scope = scope ;
295+ this .clientId = clientId ;
296+ this .iss = iss ;
267297 }
268298
269- public User getUser () {
270- return user ;
271- }
272-
273- public String getAccessToken () {
274- return accessToken ;
275- }
276-
277- public Date getExpiration () {
278- return expiration ;
279- }
280-
281- public String [] getScopes () {
282- return scopes ;
283- }
284299 }
285300}
0 commit comments