Skip to content

Commit b4ee1af

Browse files
committed
fixed introspection problems
1 parent e82fe98 commit b4ee1af

File tree

3 files changed

+66
-52
lines changed

3 files changed

+66
-52
lines changed

pom.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
88
<version>2.3.3.RELEASE</version>
9-
<relativePath/> <!-- lookup parent from repository -->
109
</parent>
1110
<groupId>cz.metacentrum</groupId>
1211
<artifactId>fake_oidc</artifactId>
13-
<version>0.0.2-SNAPSHOT</version>
12+
<version>1.0</version>
1413
<name>fake_oidc</name>
1514
<description>Fake OpenId Connect Authorization Server</description>
1615

src/main/java/cz/metacentrum/fake_oidc/FakeOidcServerProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void setTokenExpirationSeconds(long tokenExpirationSeconds) {
2828

2929
@Override
3030
public String toString() {
31-
return "FakeOidcProperties{" +
31+
return "FakeOidcServerProperties{" +
3232
"user=" + user +
3333
", tokenExpirationSeconds=" + tokenExpirationSeconds +
3434
'}';

src/main/java/cz/metacentrum/fake_oidc/OidcController.java

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,16 @@
3939
import java.util.Collections;
4040
import java.util.Date;
4141
import java.util.HashMap;
42+
import java.util.HashSet;
4243
import java.util.LinkedHashMap;
4344
import java.util.Map;
45+
import java.util.Set;
4446
import 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
5254
public 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

Comments
 (0)