-
Notifications
You must be signed in to change notification settings - Fork 141
Expand file tree
/
Copy pathDefaultIdTokenExtension.java
More file actions
173 lines (156 loc) · 6.38 KB
/
Copy pathDefaultIdTokenExtension.java
File metadata and controls
173 lines (156 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package com.sap.cloud.security.token;
import static java.util.Objects.nonNull;
import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;
import com.sap.cloud.security.xsuaa.client.OAuth2TokenResponse;
import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;
import jakarta.annotation.Nullable;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@code DefaultIdTokenExtension} provides support for resolving an ID token for the current user
* based on the token available in the {@link SecurityContext}.
*
* <p>This implementation converts an access token into an ID token by invoking the IAS token
* endpoint using the JWT bearer token grant flow.
*
* <p>Resolution behavior:
*
* <ul>
* <li>If cached ID Token is still valid, it is returned as is
* <li>If the current token is already an ID token, it is returned as-is.
* <li>If the token belongs to a technical user (claim {@code sap_id_type} = {@code "app"}, or
* {@code sub == azp} for pre-{@code sap_id_type} tokens), an exception is
* thrown.
* <li>If the token is an access token, it will be exchanged for an ID token using the configured
* IAS service credentials.
* </ul>
*
* <p>The resolved ID token can be used for authentication or downstream service calls that
* explicitly require an ID token instead of an access token.
*/
public class DefaultIdTokenExtension implements IdTokenExtension {
private static final Logger LOG = LoggerFactory.getLogger(DefaultIdTokenExtension.class);
private final OAuth2TokenService tokenService;
private final OAuth2ServiceConfiguration iasConfig;
/**
* Creates a new {@code DefaultIdTokenExtension} for exchanging access tokens into ID tokens.
*
* @param tokenService the OAuth 2.0 token service used to perform the exchange
* @param iasConfig the IAS service configuration containing client credentials
* @throws NullPointerException if any of the parameters is {@code null}
*/
public DefaultIdTokenExtension(
OAuth2TokenService tokenService, OAuth2ServiceConfiguration iasConfig) {
this.tokenService = Objects.requireNonNull(tokenService);
this.iasConfig = Objects.requireNonNull(iasConfig);
}
/**
* Resolves an ID token for the current user.
*
* <p>The current token is obtained from {@link SecurityContext#getInitialToken()} and processed
* as follows:
*
* <ul>
* <li>If the token represents a technical user, an {@link IllegalArgumentException} is thrown.
* <li>If the token is already an ID token, it is returned as-is.
* <li>If the token is an access token, it is exchanged via the IAS token endpoint.
* </ul>
*
* @return the raw JWT string of the ID token, or {@code null} if the exchange fails
* @throws IllegalArgumentException if the token belongs to a technical user
*/
@Override
public Token resolveIdToken(@Nullable Token idToken) {
if (nonNull(idToken) && !idToken.isExpired()) {
return idToken;
}
final Token token = SecurityContext.getInitialToken();
if (token == null) {
throw new IllegalArgumentException("Cannot resolve ID token with no access token present");
}
if (isTechnicalUser(token)) {
throw new IllegalArgumentException("Cannot get ID token for technical user.");
}
if (!isAccessToken(token)) {
LOG.debug("Incoming Token is already an ID Token. Returning incoming Token");
return token;
}
try {
return Token.create(exchangeAccessToIDToken(token).getAccessToken());
} catch (OAuth2ServiceException e) {
LOG.warn("Failed to retrieve ID-Token", e);
return null;
}
}
/**
* Determines whether the given token is an access token rather than an ID token.
*
* <p>This is inferred by checking whether the {@code aud} claim contains only the client ID of
* the token, which indicates an access token intended for the current client application.
*
* @param token the token to inspect
* @return {@code true} if the token is an access token, otherwise {@code false}
*/
private boolean isAccessToken(Token token) {
final List<String> audiences = token.getClaimAsStringList("aud");
return audiences.size() == 1 && !audiences.get(0).equals(token.getClientId());
}
/**
* Determines whether the token represents a technical user.
*
* <p>Prefers the {@code sap_id_type} claim ({@link SapIdType#APP}) when present. For tokens
* issued before the claim was introduced, falls back to comparing {@code sub} with
* {@code azp}.
*
* @param token the token to inspect
* @return {@code true} if the token belongs to a technical user
*/
private boolean isTechnicalUser(Token token) {
if (token instanceof SapIdToken idToken) {
SapIdType idType = idToken.getIdType();
if (idType != null) {
return idType == SapIdType.APP;
}
}
String subject = token.getClaimAsString(TokenClaims.SUBJECT);
String azp = token.getClientId();
if (subject == null || azp == null || subject.isBlank() || azp.isBlank()) {
return false;
}
return subject.equals(azp);
}
/**
* Exchanges a IAS access token for a strong IAS ID token using the JWT bearer token grant flow.
*
* @param accessToken the access IAS token to exchange
* @return the {@link OAuth2TokenResponse} containing the new ID token
* @throws OAuth2ServiceException if the exchange fails
*/
private OAuth2TokenResponse exchangeAccessToIDToken(Token accessToken)
throws OAuth2ServiceException {
final URI tokenEndpoint = URI.create(accessToken.getIssuer() + "/oauth2/token");
final Map<String, String> params = new HashMap<>();
params.put("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
params.put("assertion", accessToken.getTokenValue());
params.put("token_format", "jwt");
params.put("refresh_expiry", "0");
params.put("client_id", iasConfig.getClientId());
String appTid = accessToken.getClaimAsString("app_tid");
if (appTid != null && !appTid.isBlank()) {
params.put("app_tid", appTid);
}
return tokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
tokenEndpoint,
iasConfig.getClientIdentity(),
accessToken.getTokenValue(),
null,
params,
false);
}
}