Skip to content

Commit

Permalink
feat: userinfo for openid + console login to core + minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
matteo-s committed Feb 6, 2025
1 parent 9011190 commit 1bcf6ea
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce
return securityChain.build();
}

@Bean("authSecurityFilterChain")
public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
@Bean("tokenSecurityFilterChain")
public SecurityFilterChain tokenSecurityFilterChain(HttpSecurity http) throws Exception {
//token chain
HttpSecurity securityChain = http
.securityMatcher(new AntPathRequestMatcher("/auth/token"))
Expand All @@ -202,13 +202,9 @@ public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exc
// disable session for token requests
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

// allow cors
// always allow cors
securityChain.cors(cors -> {
if (StringUtils.hasText(corsOrigins)) {
cors.configurationSource(corsConfigurationSource(corsOrigins));
} else {
cors.disable();
}
cors.configurationSource(corsConfigurationSource("*"));
});

//enable anonymous auth, we'll double check auth in granters
Expand All @@ -232,6 +228,49 @@ public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exc
return securityChain.build();
}

@Bean("userinfoSecurityFilterChain")
public SecurityFilterChain userinfoSecurityFilterChain(HttpSecurity http) throws Exception {
//userinfo chain
HttpSecurity securityChain = http
.securityMatcher(new AntPathRequestMatcher("/auth/userinfo"))
.authorizeHttpRequests(auth -> {
auth.requestMatchers(getAuthRequestMatcher()).hasRole("USER").anyRequest().authenticated();
})
// disable request cache
.requestCache(requestCache -> requestCache.disable())
//disable csrf
.csrf(csrf -> csrf.disable())
// disable session
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

// always allow cors
securityChain.cors(cors -> {
cors.configurationSource(corsConfigurationSource("*"));
});

//disable anonymous auth
securityChain.anonymous(anon -> anon.disable());

//authentication (when configured)
if (properties.isOidcAuthEnabled() && jwtTokenService != null) {
// enable internal jwt auth provider
JwtAuthenticationProvider coreJwtAuthProvider = new JwtAuthenticationProvider(jwtTokenService.getDecoder());
coreJwtAuthProvider.setJwtAuthenticationConverter(jwtTokenService.getAuthenticationConverter());
UserAuthenticationManager authManager = authenticationManagerBuilder.build(coreJwtAuthProvider);

securityChain.authenticationManager(authManager);
securityChain.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.authenticationManager(authManager)));
}

securityChain.exceptionHandling(handling -> {
handling
.authenticationEntryPoint(new Http403ForbiddenEntryPoint())
.accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403
});

return securityChain.build();
}

@Bean("authorizeSecurityFilterChain")
public SecurityFilterChain authorizeSecurityFilterChain(HttpSecurity http) throws Exception {
//token chain
Expand Down Expand Up @@ -314,6 +353,28 @@ public SecurityFilterChain authorizeSecurityFilterChain(HttpSecurity http) throw
return securityChain.build();
}

@Bean("wellKnownSecurityFilterChain")
public SecurityFilterChain wellKnownSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(new AntPathRequestMatcher("/.well-known/**"))
.authorizeHttpRequests(auth -> {
auth.anyRequest().permitAll();
})
// disable request cache
.requestCache(requestCache -> requestCache.disable())
//disable csrf
.csrf(csrf -> csrf.disable())
// we don't want a session for these endpoints, each request should be evaluated
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// enable frame options
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()))
// always allow cors
.cors(cors -> {
cors.configurationSource(corsConfigurationSource("*"));
})
.build();
}

@Bean("h2SecurityFilterChain")
public SecurityFilterChain h2SecurityFilterChain(HttpSecurity http) throws Exception {
return http
Expand Down
2 changes: 1 addition & 1 deletion application/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ jwt:
duration: ${JWT_REFRESH_TOKEN_DURATION:}
client-id: ${JWT_CLIENT_ID:${security.basic.username}}
client-secret: ${JWT_CLIENT_SECRET:${security.basic.password}}
redirect-uris: ${JWT_REDIRECT_URIS:http://localhost:*,${application.endpoint}}
redirect-uris: ${JWT_REDIRECT_URIS:http://localhost:*,${application.endpoint}/console/auth-callback}
cache-control: ${JWKS_CACHE_CONTROL:public, max-age=900, must-revalidate, no-transform}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class ConsoleController {
@Value("${solr.url}")
private String solrUrl;

@Value("${jwt.client-id}")
private String clientId;

public static final String CONSOLE_CONTEXT = Keys.CONSOLE_CONTEXT;

@GetMapping(value = { "/", CONSOLE_CONTEXT })
Expand Down Expand Up @@ -79,11 +82,11 @@ public String console(Model model, HttpServletRequest request) {
if (securityProperties.isOidcAuthEnabled()) {
config.put("REACT_APP_AUTH_URL", "/api");
config.put("REACT_APP_LOGIN_URL", "/auth");
config.put("REACT_APP_ISSUER_URI", securityProperties.getOidc().getIssuerUri());
config.put("REACT_APP_CLIENT_ID", securityProperties.getOidc().getClientId());
if (securityProperties.getOidc().getScope() != null) {
config.put("REACT_APP_SCOPE", String.join(" ", securityProperties.getOidc().getScope()));
}
config.put("REACT_APP_ISSUER_URI", applicationUrl);
config.put("REACT_APP_CLIENT_ID", clientId);
// if (securityProperties.getOidc().getScope() != null) {
// config.put("REACT_APP_SCOPE", String.join(" ", securityProperties.getOidc().getScope()));
// }
}

config.put("REACT_APP_ENABLE_SOLR", String.valueOf(StringUtils.hasText(solrUrl)));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
package it.smartcommunitylabdhub.authorization.controllers;

import it.smartcommunitylabdhub.authorization.model.OpenIdConfig;
import it.smartcommunitylabdhub.authorization.model.OpenIdConfig.OpenIdConfigBuilder;
import it.smartcommunitylabdhub.commons.config.ApplicationProperties;
import it.smartcommunitylabdhub.commons.config.SecurityProperties;
import java.util.Collections;
import java.util.HashMap;
import it.smartcommunitylabdhub.commons.infrastructure.ConfigurationProvider;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OAuth2ConfigurationEndpoint {
public class OAuth2ConfigurationEndpoint implements ConfigurationProvider {

@Autowired
private ApplicationProperties applicationProperties;
Expand All @@ -27,10 +32,10 @@ public class OAuth2ConfigurationEndpoint {
@Value("${jwt.cache-control}")
private String cacheControl;

private Map<String, Object> config = null;
private OpenIdConfig config = null;

@GetMapping(value = { "/.well-known/openid-configuration", "/.well-known/oauth-authorization-server" })
public ResponseEntity<Map<String, Object>> getCOnfiguration() {
public ResponseEntity<Map<String, Serializable>> getConfiguration() {
if (!securityProperties.isRequired()) {
throw new UnsupportedOperationException();
}
Expand All @@ -39,32 +44,63 @@ public ResponseEntity<Map<String, Object>> getCOnfiguration() {
config = generate();
}

return ResponseEntity.ok().header(HttpHeaders.CACHE_CONTROL, cacheControl).body(config);
return ResponseEntity.ok().header(HttpHeaders.CACHE_CONTROL, cacheControl).body(config.toMap());
}

private Map<String, Object> generate() {
private OpenIdConfig generate() {
/*
* OpenID Provider Metadata
* https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
*/

String baseUrl = applicationProperties.getEndpoint();
Map<String, Object> m = new HashMap<>();
OpenIdConfigBuilder builder = OpenIdConfig.builder();

m.put("issuer", baseUrl);
m.put("jwks_uri", baseUrl + JWKSEndpoint.JWKS_URL);
m.put("response_types_supported", Collections.emptyList());
builder.issuer(baseUrl);
builder.jwksUri(baseUrl + JWKSEndpoint.JWKS_URL);
builder.responseTypesSupported(Set.of("code"));

List<String> grantTypes = Stream
.of(AuthorizationGrantType.CLIENT_CREDENTIALS, AuthorizationGrantType.REFRESH_TOKEN)
.of(
AuthorizationGrantType.CLIENT_CREDENTIALS,
AuthorizationGrantType.REFRESH_TOKEN,
AuthorizationGrantType.TOKEN_EXCHANGE
)
.map(t -> t.getValue())
.toList();
m.put("grant_types_supported", grantTypes);

m.put("token_endpoint", baseUrl + TokenEndpoint.TOKEN_URL);
List<String> authMethods = Collections.singletonList("client_secret_basic");
m.put("token_endpoint_auth_methods_supported", authMethods);
if (securityProperties.isOidcAuthEnabled()) {
grantTypes =
Stream
.of(
AuthorizationGrantType.CLIENT_CREDENTIALS,
AuthorizationGrantType.REFRESH_TOKEN,
AuthorizationGrantType.AUTHORIZATION_CODE,
AuthorizationGrantType.TOKEN_EXCHANGE
)
.map(t -> t.getValue())
.toList();

return m;
builder.authorizationEndpoint(baseUrl + AuthorizationEndpoint.AUTHORIZE_URL);
builder.userinfoEndpoint(baseUrl + UserInfoEndpoint.USERINFO_URL);
}

builder.grantTypesSupported(new HashSet<>(grantTypes));

builder.tokenEndpoint(baseUrl + TokenEndpoint.TOKEN_URL);
Set<String> authMethods = Set.of("client_secret_basic", "client_secret_post", "none");
builder.tokenEndpointAuthMethodsSupported(authMethods);

return builder.build();
}

@Override
@Nullable
public OpenIdConfig getConfig() {
if (config == null) {
config = generate();
}

return config;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Copyright 2025 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package it.smartcommunitylabdhub.authorization.controllers;

import com.nimbusds.jwt.SignedJWT;
import it.smartcommunitylabdhub.authorization.model.UserAuthentication;
import it.smartcommunitylabdhub.authorization.services.JwtTokenService;
import it.smartcommunitylabdhub.commons.config.SecurityProperties;
import java.text.ParseException;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class UserInfoEndpoint {

public static final String USERINFO_URL = "/auth/userinfo";

@Autowired
private SecurityProperties securityProperties;

@Autowired(required = false)
private JwtTokenService jwtTokenService;

@RequestMapping(value = USERINFO_URL, method = { RequestMethod.POST, RequestMethod.GET })
public Map<String, Object> userinfo(
@RequestParam Map<String, String> parameters,
@CurrentSecurityContext SecurityContext securityContext
) {
if (!securityProperties.isOidcAuthEnabled() || jwtTokenService == null) {
throw new UnsupportedOperationException();
}

Authentication authentication = securityContext.getAuthentication();

//resolve user authentication
if (
authentication == null ||
!(authentication.isAuthenticated()) ||
!(authentication instanceof UserAuthentication)
) {
throw new InsufficientAuthenticationException("Invalid or missing authentication");
}
try {
UserAuthentication<?> user = (UserAuthentication<?>) authentication;
log.debug("read userinfo for {}", user.getUsername());

//fetch token
SignedJWT token = jwtTokenService.generateAccessToken(user);
Map<String, Object> claims;

claims = token.getJWTClaimsSet().getClaims();

if (log.isTraceEnabled()) {
log.trace("userinfo: {}", claims);
}

return claims;
} catch (ParseException e) {
throw new IllegalArgumentException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,6 @@ public TokenResponse grant(@NotNull Map<String, String> parameters, Authenticati
throw new IllegalArgumentException("Invalid or missing code");
}

String state = parameters.get(OAuth2ParameterNames.STATE);
if (state == null) {
throw new IllegalArgumentException("invalid or missing state");
}

//secret auth
String cid = parameters.get(OAuth2ParameterNames.CLIENT_ID);
if (cid == null || !clientId.equals(cid)) {
Expand All @@ -153,7 +148,6 @@ public TokenResponse grant(@NotNull Map<String, String> parameters, Authenticati
.code(code)
.redirectUri(redirectUri)
.codeVerifier(codeVerifier)
.state(state)
.build();

if (log.isTraceEnabled()) {
Expand Down
Loading

0 comments on commit 1bcf6ea

Please sign in to comment.