Skip to content
This repository was archived by the owner on Sep 28, 2022. It is now read-only.

Commit ee1dda8

Browse files
dBucikDominik František Bučík
authored andcommitted
feat: 🎸 better introspectionr results
Introspect based on the actual token contents. Ommit userinfo from introspection. Implement introspection part from https://www.ietf.org/archive/id/draft-ietf-oauth-step-up-authn-challenge-02.html. BREAKING CHANGE: Requires db update (see v12.0.0.sql)
1 parent 3fcae88 commit ee1dda8

File tree

12 files changed

+242
-239
lines changed

12 files changed

+242
-239
lines changed

perun-oidc-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ CREATE TABLE IF NOT EXISTS authentication_holder_request_parameter (
8282
CREATE TABLE IF NOT EXISTS saved_user_auth (
8383
id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY,
8484
acr VARCHAR(1024),
85+
auth_time BIGINT,
8586
name VARCHAR(1024),
8687
authenticated BOOLEAN,
8788
authentication_attributes VARCHAR(2048)

perun-oidc-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ CREATE TABLE IF NOT EXISTS authentication_holder_request_parameter (
8181
CREATE TABLE IF NOT EXISTS saved_user_auth (
8282
id BIGINT AUTO_INCREMENT PRIMARY KEY,
8383
acr VARCHAR(1024),
84+
auth_time BIGINT DEFAULT NULL,
8485
name VARCHAR(1024),
8586
authenticated BOOLEAN,
8687
authentication_attributes TEXT
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE saved_user_auth ADD auth_time BIGINT DEFAULT NULL;

perun-oidc-server-webapp/src/main/resources/db/psql/psql_database_tables.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ CREATE TABLE IF NOT EXISTS authentication_holder_request_parameter (
8282
CREATE TABLE IF NOT EXISTS saved_user_auth (
8383
id BIGSERIAL PRIMARY KEY,
8484
acr VARCHAR(1024),
85+
auth_time BIGINT DEFAULT NULL,
8586
name VARCHAR(1024),
8687
authenticated BOOLEAN,
8788
authentication_attributes TEXT
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE saved_user_auth ADD COLUMN auth_time BIGINT DEFAULT NULL;

perun-oidc-server-webapp/src/main/webapp/WEB-INF/user-context.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,11 +442,6 @@
442442
<property name="perunUserInfoService" ref="userInfoService"/>
443443
</bean>
444444

445-
<bean id="introspectionResultAssembler" class="cz.muni.ics.oidc.server.PerunIntrospectionResultAssembler" primary="true">
446-
<constructor-arg name="configBean" ref="configBean"/>
447-
<constructor-arg name="translator" ref="scopeClaimTranslator"/>
448-
</bean>
449-
450445
<bean id="perunOidcConfig" class="cz.muni.ics.oidc.server.configurations.PerunOidcConfig">
451446
<property name="rpcEnabled" value="${perun.rpc.enabled}"/>
452447
<property name="rpcUrl" value="${perun.rpc.url}"/>

perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SavedUserAuthentication.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import cz.muni.ics.oidc.saml.SamlPrincipal;
2222
import java.util.Collection;
2323
import java.util.HashSet;
24+
import java.util.List;
2425
import java.util.stream.Collectors;
2526
import javax.persistence.Basic;
2627
import javax.persistence.CollectionTable;
@@ -42,13 +43,16 @@
4243
import lombok.Setter;
4344
import lombok.ToString;
4445
import org.eclipse.persistence.annotations.CascadeOnDelete;
46+
import org.joda.time.DateTime;
47+
import org.joda.time.format.ISODateTimeFormat;
4548
import org.opensaml.saml2.core.AuthnContext;
4649
import org.opensaml.saml2.core.AuthnContextClassRef;
4750
import org.opensaml.saml2.core.AuthnStatement;
4851
import org.springframework.security.core.Authentication;
4952
import org.springframework.security.core.GrantedAuthority;
5053
import org.springframework.security.providers.ExpiringUsernameAuthenticationToken;
5154
import org.springframework.security.saml.SAMLCredential;
55+
import org.springframework.util.StringUtils;
5256

5357
/**
5458
* This class stands in for an original Authentication object.
@@ -90,6 +94,9 @@ public class SavedUserAuthentication implements Authentication {
9094
@Column(name = "acr")
9195
private String acr;
9296

97+
@Column(name = "auth_time")
98+
private Long authTime = null;
99+
93100
@Column(name = "authentication_attributes")
94101
@Convert(converter = SamlAuthenticationDetailsStringConverter.class)
95102
private SamlAuthenticationDetails authenticationDetails;
@@ -100,8 +107,9 @@ public SavedUserAuthentication(Authentication src) {
100107
setAuthenticated(src.isAuthenticated());
101108
if (src instanceof SavedUserAuthentication) {
102109
SavedUserAuthentication source = (SavedUserAuthentication) src;
103-
this.setAcr(source.getAcr());
104-
this.setAuthenticationDetails(source.getAuthenticationDetails());
110+
this.setAcr(source.acr);
111+
this.setAuthTime(source.authTime);
112+
this.setAuthenticationDetails(source.authenticationDetails);
105113
} else if (src instanceof ExpiringUsernameAuthenticationToken) {
106114
ExpiringUsernameAuthenticationToken token = (ExpiringUsernameAuthenticationToken) src;
107115
SAMLCredential credential = ((SamlPrincipal) token.getPrincipal()).getSamlCredential();
@@ -113,6 +121,8 @@ public SavedUserAuthentication(Authentication src) {
113121
.map(AuthnContextClassRef::getAuthnContextClassRef)
114122
.collect(Collectors.joining())
115123
);
124+
this.setAuthTime(credential.getAuthenticationAssertion()
125+
.getAuthnStatements().get(0).getAuthnInstant().getMillis());
116126
this.setAuthenticationDetails(new SamlAuthenticationDetails(credential));
117127
}
118128
}

perun-oidc-server/src/main/java/cz/muni/ics/oauth2/service/IntrospectionResultAssembler.java

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,40 @@
2828
*/
2929
public interface IntrospectionResultAssembler {
3030

31-
String TOKEN_TYPE = "token_type";
31+
String ACTIVE = "active";
32+
String SCOPE = "scope";
3233
String CLIENT_ID = "client_id";
33-
String USER_ID = "user_id";
34-
String SUB = "sub";
34+
String USERNAME = "username";
35+
String TOKEN_TYPE = "token_type";
3536
String EXP = "exp";
36-
String EXPIRES_AT = "expires_at";
37+
String IAT = "iat";
38+
String NBF = "nbf";
39+
String SUB = "sub";
40+
String AUD = "aud";
41+
String ISS = "iss";
42+
String JTI = "jti";
43+
44+
String ACR = "acr";
45+
String AUTH_TIME = "auth_time";
3746
String SCOPE_SEPARATOR = " ";
38-
String SCOPE = "scope";
39-
String ACTIVE = "active";
40-
DateFormatter dateFormat = new DateFormatter(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"));
47+
4148

4249
/**
4350
* Assemble a token introspection result from the given access token and user info.
4451
*
45-
* @param accessToken the access token
46-
* @param userInfo the user info
47-
* @param authScopes the scopes the client is authorized for
52+
* @param token the access token
53+
* @param introspectionRequesterScopes the scopes the client is authorized for
4854
* @return the token introspection result
4955
*/
50-
Map<String, Object> assembleFrom(OAuth2AccessTokenEntity accessToken, UserInfo userInfo, Set<String> authScopes);
56+
Map<String, Object> assembleFrom(OAuth2AccessTokenEntity token, Set<String> introspectionRequesterScopes);
5157

5258
/**
5359
* Assemble a token introspection result from the given refresh token and user info.
5460
*
55-
* @param refreshToken the refresh token
56-
* @param userInfo the user info
57-
* @param authScopes the scopes the client is authorized for
61+
* @param token the refresh token
62+
* @param introspectionRequesterScopes the scopes the client is authorized for
5863
* @return the token introspection result
5964
*/
60-
Map<String, Object> assembleFrom(OAuth2RefreshTokenEntity refreshToken, UserInfo userInfo, Set<String> authScopes);
65+
Map<String, Object> assembleFrom(OAuth2RefreshTokenEntity token, Set<String> introspectionRequesterScopes);
6166

6267
}

perun-oidc-server/src/main/java/cz/muni/ics/oauth2/service/impl/DefaultIntrospectionResultAssembler.java

Lines changed: 142 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,35 @@
1515
*******************************************************************************/
1616
package cz.muni.ics.oauth2.service.impl;
1717

18-
import static com.google.common.collect.Maps.newLinkedHashMap;
19-
2018
import com.google.common.base.Joiner;
2119
import com.google.common.collect.Sets;
22-
import com.google.common.collect.Sets;
20+
import com.nimbusds.jwt.JWT;
21+
import com.nimbusds.jwt.JWTClaimsSet;
22+
import cz.muni.ics.oauth2.model.AuthenticationHolderEntity;
23+
import cz.muni.ics.oauth2.model.AuthenticationStatement;
2324
import cz.muni.ics.oauth2.model.OAuth2AccessTokenEntity;
2425
import cz.muni.ics.oauth2.model.OAuth2RefreshTokenEntity;
26+
import cz.muni.ics.oauth2.model.SamlAuthenticationDetails;
27+
import cz.muni.ics.oauth2.model.SavedUserAuthentication;
2528
import cz.muni.ics.oauth2.service.IntrospectionResultAssembler;
26-
import cz.muni.ics.openid.connect.model.UserInfo;
27-
import java.text.ParseException;
28-
import java.util.Map;
29-
import java.util.Set;
3029
import lombok.extern.slf4j.Slf4j;
30+
import org.joda.time.DateTime;
31+
import org.joda.time.format.DateTimeFormatter;
32+
import org.joda.time.format.ISODateTimeFormat;
33+
import org.springframework.security.oauth2.common.OAuth2AccessToken;
3134
import org.springframework.security.oauth2.provider.OAuth2Authentication;
3235
import org.springframework.stereotype.Service;
36+
import org.springframework.util.StringUtils;
37+
38+
import java.sql.Timestamp;
39+
import java.text.ParseException;
40+
import java.util.HashSet;
41+
import java.util.LinkedHashMap;
42+
import java.util.List;
43+
import java.util.Map;
44+
import java.util.Set;
45+
46+
import static com.google.common.collect.Maps.newLinkedHashMap;
3347

3448
/**
3549
* Default implementation of the {@link IntrospectionResultAssembler} interface.
@@ -39,81 +53,147 @@
3953
public class DefaultIntrospectionResultAssembler implements IntrospectionResultAssembler {
4054

4155
@Override
42-
public Map<String, Object> assembleFrom(OAuth2AccessTokenEntity accessToken, UserInfo userInfo, Set<String> authScopes) {
43-
44-
Map<String, Object> result = newLinkedHashMap();
45-
OAuth2Authentication authentication = accessToken.getAuthenticationHolder().getAuthentication();
46-
47-
result.put(ACTIVE, true);
48-
49-
50-
Set<String> scopes = Sets.intersection(authScopes, accessToken.getScope());
51-
result.put(SCOPE, Joiner.on(SCOPE_SEPARATOR).join(scopes));
52-
53-
if (accessToken.getExpiration() != null) {
54-
try {
55-
result.put(EXPIRES_AT, dateFormat.valueToString(accessToken.getExpiration()));
56-
result.put(EXP, accessToken.getExpiration().getTime() / 1000L);
57-
} catch (ParseException e) {
58-
log.error("Parse exception in token introspection", e);
59-
}
60-
}
61-
62-
if (userInfo != null) {
63-
// if we have a UserInfo, use that for the subject
64-
result.put(SUB, userInfo.getSub());
56+
public Map<String, Object> assembleFrom(OAuth2AccessTokenEntity token, Set<String> introspectionRequesterScopes) {
57+
AuthenticationHolderEntity authenticationHolder = token.getAuthenticationHolder();
58+
OAuth2Authentication authentication = (authenticationHolder != null) ?
59+
authenticationHolder.getAuthentication() : null;
60+
61+
Set<String> scopes = Sets.intersection(introspectionRequesterScopes, token.getScope());
62+
String scope = Joiner.on(SCOPE_SEPARATOR).join(scopes);
63+
Long exp = null;
64+
if (token.getExpiration() != null) {
65+
exp = token.getExpiration().getTime() / 1000L;
6566
} else {
66-
// otherwise, use the authentication's username
67-
result.put(SUB, authentication.getName());
67+
log.warn("WARNING - ACCESS TOKEN WITHOUT EXPIRATION DATE DETECTED ('{}')", token);
6868
}
6969

70-
if(authentication.getUserAuthentication() != null) {
71-
result.put(USER_ID, authentication.getUserAuthentication().getName());
70+
String clientId = (authentication != null && authentication.getOAuth2Request() != null) ?
71+
authentication.getOAuth2Request().getClientId() : null;
72+
if (clientId == null) {
73+
clientId = (token.getClient() != null) ? token.getClient().getClientId() : null;
7274
}
7375

74-
result.put(CLIENT_ID, authentication.getOAuth2Request().getClientId());
76+
String tokenType = OAuth2AccessToken.BEARER_TYPE;
77+
JWT jwtValue = token.getJwtValue();
78+
String username = (authenticationHolder != null
79+
&& authenticationHolder.getAuthentication() != null
80+
&& authenticationHolder.getAuthentication().getUserAuthentication() != null) ?
81+
authenticationHolder.getAuthentication().getUserAuthentication().getName() : null;
7582

76-
result.put(TOKEN_TYPE, accessToken.getTokenType());
83+
Map<String, Object> result = assemble(scope, exp, username, clientId, tokenType, jwtValue, authenticationHolder);
7784

85+
if (!token.isExpired()) {
86+
result.put(ACTIVE, true);
87+
} else {
88+
result.clear();
89+
result.put(ACTIVE, false);
90+
}
7891
return result;
7992
}
8093

8194
@Override
82-
public Map<String, Object> assembleFrom(OAuth2RefreshTokenEntity refreshToken, UserInfo userInfo, Set<String> authScopes) {
95+
public Map<String, Object> assembleFrom(OAuth2RefreshTokenEntity token, Set<String> introspectionRequesterScopes) {
96+
AuthenticationHolderEntity authenticationHolder = token.getAuthenticationHolder();
97+
OAuth2Authentication authentication = (authenticationHolder != null) ?
98+
authenticationHolder.getAuthentication() : null;
99+
Set<String> tokenScopes = (authentication != null && authentication.getOAuth2Request() != null) ?
100+
authentication.getOAuth2Request().getScope() : new HashSet<>();
101+
102+
Set<String> scopes = Sets.intersection(introspectionRequesterScopes, tokenScopes);
103+
String scope = Joiner.on(SCOPE_SEPARATOR).join(scopes);
104+
Long exp = null;
105+
if (token.getExpiration() != null) {
106+
exp = token.getExpiration().getTime() / 1000L;
107+
} else {
108+
log.warn("WARNING - REFRESH TOKEN WITHOUT EXPIRATION DATE DETECTED ('{}')", token);
109+
}
83110

84-
Map<String, Object> result = newLinkedHashMap();
85-
OAuth2Authentication authentication = refreshToken.getAuthenticationHolder().getAuthentication();
111+
String clientId = (authentication != null && authentication.getOAuth2Request() != null) ?
112+
authentication.getOAuth2Request().getClientId() : null;
113+
String username = (authentication != null && authentication.getUserAuthentication() != null) ?
114+
authentication.getUserAuthentication().getName() : null;
115+
String tokenType = "refresh_token";
116+
JWT jwtValue = token.getJwt();
86117

87-
result.put(ACTIVE, true);
118+
Map<String, Object> result = assemble(scope, exp, username, clientId, tokenType, jwtValue, authenticationHolder);
88119

89-
Set<String> scopes = Sets.intersection(authScopes, authentication.getOAuth2Request().getScope());
120+
if (!token.isExpired()) {
121+
result.put(ACTIVE, true);
122+
} else {
123+
result.clear();
124+
result.put(ACTIVE, false);
125+
}
126+
return result;
127+
}
90128

91-
result.put(SCOPE, Joiner.on(SCOPE_SEPARATOR).join(scopes));
129+
private Map<String, Object> assemble(String scope,
130+
Long exp,
131+
String username,
132+
String clientId,
133+
String tokenType,
134+
JWT jwtValue,
135+
AuthenticationHolderEntity authenticationHolder)
136+
{
137+
Map<String, Object> result = new LinkedHashMap<>();
138+
if (scope != null && !scope.isEmpty()) {
139+
result.put(SCOPE, scope);
140+
}
141+
if (StringUtils.hasText(clientId)) {
142+
result.put(CLIENT_ID, clientId);
143+
}
144+
if (StringUtils.hasText(tokenType)) {
145+
result.put(TOKEN_TYPE, tokenType);
146+
}
147+
if (exp != null) {
148+
result.put(EXP, exp);
149+
}
150+
if (StringUtils.hasText(username)) {
151+
result.put(USERNAME, username);
152+
}
153+
if (jwtValue != null) {
154+
fillDataFromJwt(jwtValue, result);
155+
}
156+
if (authenticationHolder != null && authenticationHolder.getUserAuth() != null) {
157+
fillAcrAndAuthTime(authenticationHolder.getUserAuth(), result);
158+
}
159+
return result;
160+
}
92161

93-
if (refreshToken.getExpiration() != null) {
94-
try {
95-
result.put(EXPIRES_AT, dateFormat.valueToString(refreshToken.getExpiration()));
96-
result.put(EXP, refreshToken.getExpiration().getTime() / 1000L);
97-
} catch (ParseException e) {
98-
log.error("Parse exception in token introspection", e);
162+
private void fillDataFromJwt(JWT atJwt, Map<String, Object> result) {
163+
try {
164+
JWTClaimsSet atClaimsSet = atJwt.getJWTClaimsSet();
165+
if (atClaimsSet != null) {
166+
if (atClaimsSet.getIssueTime() != null) {
167+
result.put(IAT, atClaimsSet.getIssueTime().getTime() / 1000L);
168+
}
169+
if (atClaimsSet.getNotBeforeTime() != null) {
170+
result.put(NBF, atClaimsSet.getNotBeforeTime().getTime() / 1000L);
171+
}
172+
if (StringUtils.hasText(atClaimsSet.getSubject())) {
173+
result.put(SUB, atClaimsSet.getSubject());
174+
}
175+
if (atClaimsSet.getAudience() != null) {
176+
result.put(AUD, atClaimsSet.getAudience());
177+
}
178+
if (StringUtils.hasText(atClaimsSet.getIssuer())) {
179+
result.put(ISS, atClaimsSet.getIssuer());
180+
}
181+
if (StringUtils.hasText(atClaimsSet.getJWTID())) {
182+
result.put(JTI, atClaimsSet.getJWTID());
183+
}
99184
}
185+
} catch (ParseException e) {
186+
log.warn("Caught exception while introspecting token and parsing JWT value '{}'", atJwt, e);
100187
}
188+
}
101189

102-
103-
if (userInfo != null) {
104-
// if we have a UserInfo, use that for the subject
105-
result.put(SUB, userInfo.getSub());
106-
} else {
107-
// otherwise, use the authentication's username
108-
result.put(SUB, authentication.getName());
190+
private void fillAcrAndAuthTime(SavedUserAuthentication savedUserAuthentication, Map<String, Object> result) {
191+
if (StringUtils.hasText(savedUserAuthentication.getAcr())) {
192+
result.put(ACR, savedUserAuthentication.getAcr());
109193
}
110-
111-
if(authentication.getUserAuthentication() != null) {
112-
result.put(USER_ID, authentication.getUserAuthentication().getName());
194+
if (savedUserAuthentication.getAuthTime() != null) {
195+
result.put(AUTH_TIME, savedUserAuthentication.getAuthTime());
113196
}
114-
115-
result.put(CLIENT_ID, authentication.getOAuth2Request().getClientId());
116-
117-
return result;
118197
}
198+
119199
}

0 commit comments

Comments
 (0)