Skip to content

Commit 9c2da33

Browse files
committed
Allow configuring the header name that carries the JWT, and optionally specify a prefix for its value.
1 parent fafa704 commit 9c2da33

File tree

2 files changed

+130
-20
lines changed

2 files changed

+130
-20
lines changed

webapp/src/main/java/com/box/l10n/mojito/security/SecurityConfig.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class SecurityConfig {
3434

3535
Map<String, OAuth2> oAuth2 = new HashMap<>();
3636

37+
Jwt jwt;
38+
3739
public List<AuthenticationType> getAuthenticationType() {
3840
return authenticationType;
3941
}
@@ -58,6 +60,14 @@ public void setoAuth2(Map<String, OAuth2> oAuth2) {
5860
this.oAuth2 = oAuth2;
5961
}
6062

63+
public Jwt getJwt() {
64+
return jwt;
65+
}
66+
67+
public void setJwt(Jwt jwt) {
68+
this.jwt = jwt;
69+
}
70+
6171
/** Types of authentication available */
6272
public enum AuthenticationType {
6373
LDAP,
@@ -136,4 +146,29 @@ public void setUsernameFromEmail(boolean usernameFromEmail) {
136146
this.usernameFromEmail = usernameFromEmail;
137147
}
138148
}
149+
150+
public static class Jwt {
151+
152+
/** Optional alternate header that carries the access token when not using Authorization. */
153+
String tokenHeaderName;
154+
155+
/** Optional prefix (e.g. "Bearer") stripped from the configured token header value. */
156+
String tokenHeaderPrefix;
157+
158+
public String getTokenHeaderName() {
159+
return tokenHeaderName;
160+
}
161+
162+
public void setTokenHeaderName(String tokenHeaderName) {
163+
this.tokenHeaderName = tokenHeaderName;
164+
}
165+
166+
public String getTokenHeaderPrefix() {
167+
return tokenHeaderPrefix;
168+
}
169+
170+
public void setTokenHeaderPrefix(String tokenHeaderPrefix) {
171+
this.tokenHeaderPrefix = tokenHeaderPrefix;
172+
}
173+
}
139174
}

webapp/src/main/java/com/box/l10n/mojito/security/WebSecurityJWTConfig.java

Lines changed: 95 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.box.l10n.mojito.entity.security.user.User;
55
import com.box.l10n.mojito.service.security.user.UserService;
66
import jakarta.annotation.PostConstruct;
7+
import jakarta.servlet.http.HttpServletRequest;
78
import java.util.ArrayList;
89
import java.util.List;
910
import org.slf4j.Logger;
@@ -20,6 +21,7 @@
2021
import org.springframework.security.config.http.SessionCreationPolicy;
2122
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2223
import org.springframework.security.oauth2.jwt.Jwt;
24+
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
2325
import org.springframework.security.web.SecurityFilterChain;
2426
import org.springframework.util.StringUtils;
2527

@@ -33,14 +35,20 @@ public class WebSecurityJWTConfig {
3335

3436
UserService userService;
3537

38+
SecurityConfig securityConfig;
39+
3640
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri:}")
3741
String issuerUri;
3842

3943
@Value("${spring.security.oauth2.resourceserver.jwt.audience:}")
4044
String audience;
4145

42-
public WebSecurityJWTConfig(UserService userService) {
46+
private final DefaultBearerTokenResolver defaultBearerTokenResolver;
47+
48+
public WebSecurityJWTConfig(UserService userService, SecurityConfig securityConfig) {
4349
this.userService = userService;
50+
this.securityConfig = securityConfig;
51+
this.defaultBearerTokenResolver = new DefaultBearerTokenResolver();
4452
}
4553

4654
@PostConstruct
@@ -59,17 +67,15 @@ public void validateStatelessConfig() {
5967
@Order(2)
6068
SecurityFilterChain security(HttpSecurity http) throws Exception {
6169

62-
http.securityMatcher(
63-
req -> {
64-
String h = req.getHeader("Authorization");
65-
return h != null && h.startsWith("Bearer ");
66-
});
70+
http.securityMatcher(req -> StringUtils.hasText(resolveAccessToken(req)));
6771

6872
applyStatelessSharedConfig(http);
6973

7074
http.oauth2ResourceServer(
7175
oauth ->
72-
oauth.jwt(
76+
oauth
77+
.bearerTokenResolver(this::resolveAccessToken)
78+
.jwt(
7379
jwtConfigurer -> {
7480
jwtConfigurer.jwtAuthenticationConverter(
7581
jwt -> {
@@ -128,7 +134,7 @@ public static void applyStatelessSharedConfig(HttpSecurity http) throws Exceptio
128134

129135
private User ensureUserFromJwt(Jwt jwt) {
130136
String issuer = jwt.getIssuer() != null ? jwt.getIssuer().toString() : null;
131-
IdStrategy strategy = pickStrategy(issuer);
137+
IdentityProviderType providerType = resolveProviderType(issuer);
132138

133139
String email = jwt.getClaimAsString("email");
134140
String upn = jwt.getClaimAsString("preferred_username");
@@ -139,10 +145,13 @@ private User ensureUserFromJwt(Jwt jwt) {
139145
String family = jwt.getClaimAsString("family_name");
140146

141147
String username =
142-
switch (strategy) {
143-
case AZURE_AD -> firstNonBlank(localPart(upn), localPart(email), sub, oid);
144-
case CLOUDFLARE -> firstNonBlank(localPart(email), sub, oid, localPart(upn));
145-
case AUTO -> firstNonBlank(localPart(email), localPart(upn), sub, oid);
148+
switch (providerType) {
149+
case AZURE_AD ->
150+
firstNonBlank(localPart(upn), localPart(email), sub, oid);
151+
case CLOUDFLARE ->
152+
firstNonBlank(localPart(email), sub, oid, localPart(upn));
153+
case AUTO ->
154+
firstNonBlank(localPart(email), localPart(upn), sub, oid);
146155
};
147156

148157
if (!StringUtils.hasText(username)) {
@@ -158,17 +167,81 @@ private User ensureUserFromJwt(Jwt jwt) {
158167
return userService.getOrCreateOrUpdateBasicUser(username, given, family, name);
159168
}
160169

161-
private IdStrategy pickStrategy(String issuer) {
170+
private String resolveAccessToken(HttpServletRequest request) {
171+
if (request == null) {
172+
return null;
173+
}
174+
175+
CustomTokenHeader customTokenHeader = getCustomTokenHeader();
176+
if (customTokenHeader == null) {
177+
String token = defaultBearerTokenResolver.resolve(request);
178+
if (StringUtils.hasText(token)) {
179+
return token;
180+
}
181+
return null;
182+
}
183+
184+
String headerValue = request.getHeader(customTokenHeader.name());
185+
if (!StringUtils.hasText(headerValue)) {
186+
return null;
187+
}
188+
189+
String candidate = headerValue.trim();
190+
if (!StringUtils.hasText(candidate)) {
191+
return null;
192+
}
193+
194+
String prefix = customTokenHeader.prefix();
195+
if (!StringUtils.hasText(prefix)) {
196+
return candidate;
197+
}
198+
199+
if (candidate.startsWith(prefix)) {
200+
String stripped = candidate.substring(prefix.length()).trim();
201+
return StringUtils.hasText(stripped) ? stripped : null;
202+
}
203+
204+
return null;
205+
}
206+
207+
private CustomTokenHeader getCustomTokenHeader() {
208+
SecurityConfig.Jwt jwtConfig = securityConfig.getJwt();
209+
if (jwtConfig == null) {
210+
return null;
211+
}
212+
213+
String headerName = jwtConfig.getTokenHeaderName();
214+
if (!StringUtils.hasText(headerName)) {
215+
return null;
216+
}
217+
218+
String name = headerName.trim();
219+
if (!StringUtils.hasText(name)) {
220+
return null;
221+
}
222+
223+
String prefix = jwtConfig.getTokenHeaderPrefix();
224+
if (StringUtils.hasText(prefix)) {
225+
prefix = prefix.trim();
226+
prefix = StringUtils.hasText(prefix) ? prefix : null;
227+
} else {
228+
prefix = null;
229+
}
230+
231+
return new CustomTokenHeader(name, prefix);
232+
}
233+
234+
private IdentityProviderType resolveProviderType(String issuer) {
162235
if (issuer == null) {
163-
return IdStrategy.AUTO;
236+
return IdentityProviderType.AUTO;
164237
}
165238
if (issuer.contains("login.microsoftonline.com")) {
166-
return IdStrategy.AZURE_AD;
239+
return IdentityProviderType.AZURE_AD;
167240
}
168241
if (issuer.contains("cloudflareaccess.com")) {
169-
return IdStrategy.CLOUDFLARE;
242+
return IdentityProviderType.CLOUDFLARE;
170243
}
171-
return IdStrategy.AUTO;
244+
return IdentityProviderType.AUTO;
172245
}
173246

174247
private String firstNonBlank(String... values) {
@@ -194,12 +267,14 @@ private String localPart(String value) {
194267
return value.trim();
195268
}
196269

197-
private enum IdStrategy {
270+
private enum IdentityProviderType {
198271
AUTO,
199-
AZURE_AD,
200-
CLOUDFLARE
272+
AZURE_AD,
273+
CLOUDFLARE
201274
}
202275

276+
private record CustomTokenHeader(String name, String prefix) {}
277+
203278
static class StatelessAuthenticationToken extends AbstractAuthenticationToken {
204279

205280
UserDetailsImpl userDetails;

0 commit comments

Comments
 (0)