I have the following controller to get registration ClientA when the endpoint is called.
#GetMapping("/token")
fun token(
#RegisteredOAuth2AuthorizedClient("clientA") authorizedClient: OAuth2AuthorizedClient
): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity(token, HttpStatus.OK)
}
I want to have the client as a query param and dynamically start the OAuth2 process. How could I achieve it? sth like the following:
#GetMapping("/token?client={client}")
fun token(
#RegisteredOAuth2AuthorizedClient(${client}) authorizedClient: OAuth2AuthorizedClient
): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity(token, HttpStatus.OK)
}
My solution is to use the default authorization uri and defaultSuccessUrl("/token")
The goal is to scale the client easily, and this solution can achieve the same.
Solution:
Register clientA in application.yml
clientA:
client-id: Any
redirect-uri: http://localhost/index
provider: clientA-provider
scope: launch
client-name: clientA
client-authentication-method: none
authorization-grant-type: authorization_code
Now the endpoint /oauth2/authorization/clientA is created automatically by spring security. For getting a token, call the above url.
After successful authorization, the endpoint goes to /token, due to defaultSuccessUrl("/token").
The following shows the related code snippet:
override fun configure(http: HttpSecurity) {
http.csrf()
.disable()
.oauth2Login()
.authorizationEndpoint()
.and()
.tokenEndpoint()
.and()
.defaultSuccessUrl("/token")
#Controller
#RequestMapping("/token")
class LaunchController(private val tokenService: TokenService) {
#GetMapping
fun token(): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity.ok(token)
}
}
Related
We have problems with obtaining ID token using auth0 SDK.
We have API Gateway based on Spring Cloud Gateway (version 3.1.4) where we try to use your auth0 platform to authenticate the users and then route the exchange to our micro services. To do it we would like to use ID Token and get email from it and pass this email to our micro services.
We log in by hitting oauth2/authorization/auth0 endpoint, we are being redirected to auth0 login page, where we provide credentials, then we get redirect back to our app.
When we configuire endpoints directly in API Gateway and mark them with #AuthenticationPrincipal OidcUser user it works and we have full user details as well as ID token.
When we proxy the exchange to different service we have Authorisation header in the request, which contains only header & signature part without payload in the ID token.
We would need the payload in ID Token in order to fetch user email for mapping the user with our internal DB in our micro services.
What do you think would be the proper workflow in this case and how can we solve this issue?
We tried to use Rules & Actions, which I paste below, but it didn’t helped us.
Our configuration looks like this:
#Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/test").authenticated()
.anyExchange().authenticated()
.and().oauth2Login()
.and().logout().logoutSuccessHandler(logoutSuccessHandler())
.and().build();
}
In the RouteLocator in GatewayConfiguration we have filter for TokenRelay.
Our Action looks like this:
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'http://test.{our_local_development_route}:8888';
if (event.authorization) {
api.idToken.setCustomClaim(`${namespace}/claims/email`, event.user.email);
api.accessToken.setCustomClaim(`${namespace}/email`, event.user.email);
}
};
And Rule:
function addEmailToAccessToken(user, context, callback) {
// This rule adds the authenticated user's email address to the access token.
const namespace = 'http://test.{our_local_development_route}:8888';
context.idToken[namespace + 'email'] = user.upn;
context.accessToken[namespace + 'email'] = user.email;
return callback(null, user, context);
}
I recently created a microservices architecture with Spring Cloud Gateway and Auth0. I wrote about how I created it on the Auth0 blog. That's not the interesting part. The interesting part is JHipster generates a ReactiveJwtDecoder that calls a /userinfo endpoint if some claims aren't available in the access token. This way, the access token is enriched with identity information before it's relayed to downstream microservices.
#Bean
ReactiveJwtDecoder jwtDecoder(ReactiveClientRegistrationRepository registrations) {
Mono<ClientRegistration> clientRegistration = registrations.findByRegistrationId("oidc");
return clientRegistration
.map(oidc ->
createJwtDecoder(
oidc.getProviderDetails().getIssuerUri(),
oidc.getProviderDetails().getJwkSetUri(),
oidc.getProviderDetails().getUserInfoEndpoint().getUri()
)
)
.block();
}
private ReactiveJwtDecoder createJwtDecoder(String issuerUri, String jwkSetUri, String userInfoUri) {
NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return new ReactiveJwtDecoder() {
#Override
public Mono<Jwt> decode(String token) throws JwtException {
return jwtDecoder.decode(token).flatMap(jwt -> enrich(token, jwt));
}
private Mono<Jwt> enrich(String token, Jwt jwt) {
// Only look up user information if identity claims are missing
if (jwt.hasClaim("given_name") && jwt.hasClaim("family_name")) {
return Mono.just(jwt);
}
// Retrieve user info from OAuth provider if not already loaded
return users.get(
jwt.getSubject(),
s -> {
WebClient webClient = WebClient.create();
return webClient
.get()
.uri(userInfoUri)
.headers(headers -> headers.setBearerAuth(token))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.map(userInfo ->
Jwt
.withTokenValue(jwt.getTokenValue())
.subject(jwt.getSubject())
.audience(jwt.getAudience())
.headers(headers -> headers.putAll(jwt.getHeaders()))
.claims(claims -> {
String username = userInfo.get("preferred_username").toString();
// special handling for Auth0
if (userInfo.get("sub").toString().contains("|") && username.contains("#")) {
userInfo.put("email", username);
}
// Allow full name in a name claim - happens with Auth0
if (userInfo.get("name") != null) {
String[] name = userInfo.get("name").toString().split("\\s+");
if (name.length > 0) {
userInfo.put("given_name", name[0]);
userInfo.put("family_name", String.join(" ", Arrays.copyOfRange(name, 1, name.length)));
}
}
claims.putAll(userInfo);
})
.claims(claims -> claims.putAll(jwt.getClaims()))
.build()
);
}
);
}
};
}
I have security configuration for my webflux server:
#Bean
fun httpTestFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/webjars/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyExchange().access(authManager)
.and().cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
return http.build()
}
#Bean
fun userDetailsService(): ReactiveUserDetailsService {
val user: UserDetails = User.builder()
.username(userName)
.password(passwordEncoder().encode(password))
.roles("ADMIN")
.build()
return MapReactiveUserDetailsService(user)
}
#Bean
fun passwordEncoder() = BCryptPasswordEncoder()
#Bean("CustomAuth")
fun authManager():
ReactiveAuthorizationManager<AuthorizationContext> {
return ReactiveAuthorizationManager<AuthorizationContext> { mono, context ->
val request = context.exchange.request
val mutateExchange = context.exchange.mutate()
val token = request.headers[AUTHORIZATION] ?: throw
AccessDeniedException(ERROR_MESSAGE)
mono
// go to other service to check token
.then(webClient.checkToken(token.first().toString()))
.doOnError {
throw AccessDeniedException(ERROR_MESSAGE)
}
.cast(ResponseEntity::class.java)
.map { it.body as AuthDto }
.doOnNext { auth ->
mutateExchange.request {
it.header(USER_ID, auth.userId.toString())
it.header(AUTH_SYSTEM, auth.authSystem)
}
}
.map { AuthorizationDecision(true) }
}
}
As you can see httpBasic() option is disabled. When I go to any secure url, browser shows http basic window. Then I can enter valid or INVALID login and password and if authManager returns good result authentication will be successful or 401 will thrown in other case and auth window in browser will reopen.
Why it happens? Is it bug?
P.S. Spring boot version 2.5.5
The solution helped me:
#Bean
fun httpTestFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/webjars/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyExchange().access(authManager)
.and().cors()
.and()
.exceptionHandling()
.authenticationEntryPoint { exchange, _ ->
val response = exchange.response
response.statusCode = HttpStatus.UNAUTHORIZED
response.headers.set(HttpHeaders.WWW_AUTHENTICATE, "None")
exchange.mutate().response(response)
Mono.empty()
}
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
return http.build()
}
We need change authenticationEntryPoint in part of status code and disabling HttpHeaders.WWW_AUTHENTICATE. Then return changed response.
I need to build a kind of Java Proxy+ that handle the OAuth2 flow. The idea is to login in this "Proxy+" and do the OAuth2 flow until the Proxy+ has received the token + refresh token.
After that you login onto the Proxy+ with Username and Password or something else that give you a session. The proxy will handle generically all your web requests and add the Oauth2 token to each request. If necessary the also refresh the token.
How would you implement the Proxy Part where requests are taken and enhanced and maybe a token refresh is requested. This question is NOT about the OAuth2 flow and NOT about how to get the token in the first place.
I think i try a servlet filter that intercepts all requests and enhance the request. This way it is also generic to all urls called. Any better ideas?
Found a better way, this is the Spring way of a generic Proxy :-)
#RequestMapping("/**")
public ResponseEntity<byte[]> genericRequest(RequestEntity<?> inboundRequestEntity, HttpServletRequest request) {
URI outboundUri = UriComponentsBuilder.fromHttpUrl(this.targetBaseUrl)
.path(removeUrlPart(request))
.query(request.getQueryString())
.build(true)
.toUri();
HttpHeaders headers = filterHeaders(inboundRequestEntity.getHeaders());
BodyBuilder builder = RequestEntity
.method(requireNonNull(inboundRequestEntity.getMethod()), outboundUri)
.headers(headers);
RequestEntity<?> outboundRequestEntity = inboundRequestEntity.hasBody() ? builder.body(requireNonNull(inboundRequestEntity.getBody())) : builder.build();
try {
LOGGER.info("Will call url '{}' with method '{}'", outboundRequestEntity.getUrl(), outboundRequestEntity.getMethod());
ResponseEntity<byte[]> responseEntity = this.restTemplate.exchange(outboundRequestEntity);
return ResponseEntity.status(responseEntity.getStatusCode())
.headers(filterHeaders(responseEntity.getHeaders()))
.body(responseEntity.getBody());
} catch (
HttpStatusCodeException e) {
return ResponseEntity.status(e.getRawStatusCode())
.headers(filterHeaders(e.getResponseHeaders()))
.body(e.getResponseBodyAsByteArray());
}
}
private static HttpHeaders filterHeaders(HttpHeaders originalHeaders) {
HttpHeaders filteredResponseHeaders = new HttpHeaders();
filteredResponseHeaders.putAll(originalHeaders);
filteredResponseHeaders.remove(CONTENT_LENGTH);
filteredResponseHeaders.remove(DATE);
return filteredResponseHeaders;
}
I am implementing a client with Spring Security OAuth2 to use resources through an API, I have to customize the request like this:
POST https://example.com/v2/oauth2/token HTTP / 1.1
Authorization: Basic xxxXXXXxxXXXXXXxXXxxXX
Content-Type: application / x-www-form-urlencoded
Accept: application / json; charset = UTF-8
grant_type = authorization_code & code = 12345678901234567890
Where: grant_type = authorization_code & code = 12345678901234567890
it is contained in the body as raw data.
How can I put the grant_type and code parameters on the body?
At the moment my code is formalized in this way:
AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails ();
resource.setAccessTokenUri ( "https://example.com/v2/oauth2/token");
resource.setClientId ( "xxxxx");
resource.setClientSecret ( "xxxxx");
resource.setGrantType ( "authorization_code");
resource.setUseCurrentUri (false);
AccessTokenRequest atr = new DefaultAccessTokenRequest ();
atr.setPreservedState (new Object ());
atr.setAuthorizationCode(authCode);
AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider ();
try {
OAuth2AccessToken accessToken = provider.obtainAccessToken (resource, atr);
}
catch (Exception e) {
System.out.println ("DEBUG" + e);
}
Maybe you can use a filter to intercept the request to the token endpoint before the authorization server completes the process of authorization.
This is my implementation of OAuth2 using a json as a body instead of URL-encoded format.
https://github.com/aldwindelgado/spring-boot-oauth2-server/blob/master/src/main/java/com/github/oauth2/server/JsonToUrlEncodedAuthenticationFilter.java
Add atr.setAuthorizationCode("12345678901234567890"); to set the auth code to the Access Token Request.
I have a spring security application and I'm trying implement websockets using Stomp.
The application is mainly REST based, using tokens for security. All requests coming in have to a security token in the header.
The problem is when setting up a simple Stomp client using basic html, spring appears to not be seeing any headers.
The client works fine if I disable the security, in which case no headers are passed in.
var socket = new SockJS('http://localhost:8080/project/ws/wsendpoint');
var headers = {'Auth': 'some_auth_token'}
writeConsole("Created socket");
stompClient = Stomp.over(socket);
stompClient.connect(headers, function(frame) {
writeConsole("Connected to via WebSocket");
stompClient.subscribe('/topic/push', function(message)
{ writeConsole(message.body);}, headers );
});
window.onbeforeunload = disconnectClient;
Heres the relevant spring configuration
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().frameOptions().sameOrigin()
.and()
.authorizeRequests()
.anyRequest().authenticated() authenticated.
.and()
.anonymous().disable()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
http.addFilterBefore(authenticationTokenFilter(), BasicAuthenticationFilter.class);
}
The doFilter in the authenticationTokenFilter class should see the header field 'Auth', as set in the client, however nothing is there.
Instead of sending header, you can replace sessionId with your own ID.
var sessionId = utils.random_string(36);
var socket = new SockJS('/socket', [], {
sessionId: () => {
return sessionId
}
});
stompClient = Stomp.over(socket);
stompClient.connect(headers, function(frame) {
writeConsole("Connected to via WebSocket");
stompClient.subscribe('/topic/push', function(message)
{ writeConsole(message.body);}, headers );
});
Stomp cannot send any custom headers during the initial authentication stage. The way round this was to send the authentication token as a query parameter (would not recommended for non-closed systems).