I'm new on OAuth2 and Spring Cloud Gateway(And WebFlux things).
My team decided to move from Zuul gateway to Spring Cloud Gateway.
And current Spring Cloud version is "Greenwich.SR1"
The problem is spring cloud gateway always response 401.
How to pass access token on Spring Cloud Gateway properly?
Auth server :
#EnableEurekaClient
#EnableAuthorizationServer
#SpringBootApplication
public class AuthServer {...} // jwtAccessTokenConverter bean included
Zuul server is :
#EnableEurekaClient
#EnableZuulProxy
#SpringBootApplication
public class ZuulServer {...}
Zuul server properties :
zuul:
sensitive-headers: Cookie,Set-Cookie
ignored-services: '*'
routes:
auth: /auth/**
Spring Cloud Gateway Server properties :
spring:
cloud:
gateway:
routes:
- id: auth
uri: lb://auth
predicates:
- Method=POST
- Path=/auth/**
filters:
- RemoveRequestHeader= Cookie,Set-Cookie
- StripPrefix=1
Spring Cloud server build.gradle :
plugins {
id 'java'
id "io.freefair.lombok" version "3.2.0"
id "org.springframework.boot" version "2.1.5.RELEASE"
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
version = '1.0.0-SNAPSHOT'
description = 'edge-service2'
sourceCompatibility = '11'
dependencies {
implementation platform("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion")
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.cloud:spring-cloud-starter-netflix-eureka-client"
implementation "org.springframework.cloud:spring-cloud-starter-netflix-ribbon"
implementation "org.springframework.cloud:spring-cloud-starter-netflix-hystrix"
implementation('org.springframework.cloud:spring-cloud-starter-gateway')
implementation "org.springframework.cloud:spring-cloud-config-client"
implementation "de.codecentric:spring-boot-admin-starter-client:$springBootAdminVersion"
implementation "net.gpedro.integrations.slack:slack-webhook:1.4.0"
testImplementation "org.springframework.boot:spring-boot-starter-test"
}
springBoot {
buildInfo()
}
bootJar {
archiveName "${project.name}.jar"
}
There is a feature in Spring Cloud Security for relaying the access token to downstream services via Spring Cloud Gateway: https://cloud.spring.io/spring-cloud-static/spring-cloud-security/2.1.3.RELEASE/single/spring-cloud-security.html#_token_relay
Simply use the TokenRelay Filter for your route or default configuration.
However, this forwards just the access token. "The access token is the artifact that allows the client application to access the user's resource"[1], whereas "an ID token is an artifact that proves that the user has been authenticated"[1], and it also contains the user attributes.
It seems that's what you want anyway, but for all the people out there that use OIDC and want to relay the ID Token, here's some more information.
Write a custom GatewayFilterFactory where you:
get the authenticated Principal via exchange.getPrincipal().ofType(OAuth2AuthenticationToken.class)
map it until you get an oAuth2User Objekt
cast it to an OidcUser
now you can do oidcUser.getIdToken.getTokenValue()
put it into a header of your choice and thus you can also forward the ID Token, not only the Access Token.
[1] https://auth0.com/blog/id-token-access-token-what-is-the-difference/
Related
I have set up an OAuth2-based installation (spring-security-oauth2-core 5.2.4) of JHipster (3.9.0) against a gitlab repository. Mainly following https://www.jhipster.tech/security/#oauth2 but using gitlab instead of keycloak.
This is an excerpt from the application.yml:
spring: ...
security:
oauth2:
client:
provider:
oidc:
issuer-uri: https://gitlab.mysite.com
registration:
oidc:
client-id: 1234123412322312312312312312321312321312
client-secret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Indeed the oauth2-authentication works well, until the session internally expires. When the client then returns the jsession-Id cookie, the underlying undertow-infrastructure throws a java.lang.IllegalStateException: UT000010: Session is invalid.
It seems that org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository.saveAuthorizationRequest stores the authorizationRequest in the user session not caring about session management configuration: (see last line below)
#Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request,
HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
if (authorizationRequest == null) {
this.removeAuthorizationRequest(request, response);
return;
}
String state = authorizationRequest.getState();
Assert.hasText(state, "authorizationRequest.state cannot be empty");
Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(request);
authorizationRequests.put(state, authorizationRequest);
request.getSession().setAttribute(this.sessionAttributeName, authorizationRequests);
}
This approach seems to contradict the stateless JWT-based authentication approach of JHipster (see e.g. https://github.com/jhipster/generator-jhipster/issues/8627).
Anyway the spring2 OAuth2 management seems to counterfeit classical load balancing approaches.
Is there a way to configure OAuth2 management in spring to work stateless with GitLab?
I have a claim named user_name within my JWT and also corresponding user-name-attribute set as user_name in spring security oauth2 client provider proper property:
spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=user_name
I can also see this property is properly being read by ReactiveClientRegistrationRepository class (ClientRegistration.ProviderDetails.UserInfoEndpoint). But when I read SecurityContextHolder.getContext().getAuthentication().getName() on Resource Server I can see the value taken from (default) sub - IdTokenClaimNames.SUB claim.
Why is that? Do I still miss some additional configuration also on resource server side to have specified user-name-attribute taken and returned by SecurityContextHolder.getContext().getAuthentication().getName() on Resource Server? I understand that only Bearer token (and maybe some cookies) is being sent from client to resource server so maybe also some other filter is needed on Gateway/client side - just guessing?
The reason is that you are using a property prefixed with security.oauth2.client, which is intended for OAuth 2.0 Clients.
In Spring Security 5.2.x, there is no Spring Boot property to indicate a user name attribute to Resource Server, e.g. security.oauth2.resourceserver.xyz
You could publish your own Converter to the DSL, though:
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
http
.oauth2ResourceServer()
.jwtAuthenticationConverter(jwt -> {
JwtAuthenticationToken authentication = converter.convert(jwt);
return new JwtAuthenticationToken(authentication.getToken(),
authentication.getAuthorities(), jwt.getClaim("claim"));
});
I'm new to ServiceStack and using it to provide an endpoint that will receive incoming requests from a remote service. No end user is involved.
The authentication flow goes like this (as specified by the author of the remote service):
"Their" remote service calls "our" endpoint, with JWT in header
"Our" endpoint extracts the 'kid' from the JWT
"Our" endpoint calls "their" oauth endpoint, with 'kid' as parameter
"Their" oauth endpoint returns a public key in form of a JWK (RS256)
"Our" endpoint verifies the JWT signature using the JWK
Does ServiceStack support this authentication flow?
I think I need to write code that hooks into the request's authentication and does steps 2-5 above. Is that right?
Edit:
I found this answer which looks to be what I'm after, i.e. custom AuthProvider that overrides PreAuthenticate with steps 2-5 above.
using System;
using ServiceStack;
using ServiceStack.Auth;
using ServiceStack.Web;
namespace MyService
{
public class CustomJwtAuthProvider : AuthProvider, IAuthWithRequest
{
public CustomJwtAuthProvider ()
{
Provider = "CustomJwtAuthProvider";
AuthRealm = "/auth/CustomJwtAuthProvider";
}
public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null)
{
return session.IsAuthenticated;
}
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
{
throw new NotImplementedException("Authenticate() should not be called directly");
}
public void PreAuthenticate(IRequest req, IResponse res)
{
// Get kid from JWT
// Get public JWK from oauth endpoint
// Verify JWT signature using JWK
if ( /* JWT sig verified */ )
{
req.Items[Keywords.Session] = new AuthUserSession
{
IsAuthenticated = true,
};
}
}
}
}
Then in the ApplicationHost.Configure():
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new CustomJwtAuthProvider(),
}));
Does this approach seem right? Do I need to hand-roll the JWT authentication, or can I leverage ServiceStack's built in features and plugins more?
For them to be able to send you a JWT that ServiceStack accepts as an Authenticated Request, your App would need to be configured with either their AES Key if they're using HMAC-SHA* algorithm or their public RSA key if they're using JWE.
This flow is very strange, for them to be able to send you a custom JWT Key they would need to be able to craft their own JWT Key which means they need either the AES or private RSA Key your App is configured with where they'd be the only ones you will be able to authenticate with your App via JWT?
It's very unlikely that you'll want to configuring your App with that of a remote Service, instead you should probably use a JWT library like the JWT NuGet Package to just verify the JWT they send you is from them, then extract the KID, call their endpoint and validate the JWK they send you using a .NET library like JWK to verify their key.
Note this flow is independent from ServiceStack's JWT Support which you would use to enable stateless authentication to your Services via JWT. Here you're just using JWT and JWK libraries to verify their keys and extract required info from them.
I'm creating an application integrating with Shopify's API, which uses OAuth2 for authentication and authorization. Using the tutorial for Spring Security OAuth2, and the tutorial for Shopify, I've been able to get integration working with a single shop. The YAML configuration looks like this:
shopify:
shop: myshop
scopes: read_customers,read_orders
security:
oauth2:
client:
clientId: myclientid
clientSecret: mysecret
tokenName: access_token
authenticationScheme: query
clientAuthenticationScheme: form
accessTokenUri: https://${shopify.shop}.myshopify.com/admin/oauth/access_token
userAuthorizationUri: https://${shopify.shop}.myshopify.com/admin/oauth/authorize?scope=${shopify.scopes}&grant_options[]=
pre-established-redirect-uri: https://myapp/login
registered-redirect-uri: https://myapp/login
use-current-uri: false
resource:
userInfoUri: https://${shopify.shop}.myshopify.com/admin/shop.json
However, this static configuration won't work for an app published in Shopify's App Store because the redirect, access token, user info, and user authorization URIs depend on the shop name. There are examples of using more than one provider, but they still have to be static.
To allow these URI's to be dynamic, I've come up with a few possible options:
Use a parameter in the /login path to identify the shop, then create a filter that adds the shop name to a ThreadLocal that runs before everything else, then dynamically create the AuthorizationCodeResourceDetails that is needed by the OAuth2 filter via a Spring proxied factory bean.
Use a sort of "metafilter" that dynamically recreates the OAuth2ClientAuthenticationProcessingFilter per request along with all of the resources that it needs.
Override OAuth2ClientAuthenticationProcessingFilter so that it can handle recreating the RestTemplate it needs to obtain the access token.
All of these options seem pretty difficult. What's a good way to handle dynamically-generated URI's for access tokens and user information in Spring Security OAuth2?
Also, since I'm new to OAuth2 in general, do I need to enable a Resource Server in my Spring configuration to protect my app with the access token?
a bit late but I did return a dynamic url for an oauth resource by overriding the getter for the Oauth2ProtectedResource
#Bean(name = "googleOauthResource")
public BaseOAuth2ProtectedResourceDetails getGoogleOauthResource() {
final AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails() {
#Override
public String getPreEstablishedRedirectUri() {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
final HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
return request.getRequestURL() + "?" + request.getQueryString() + "&addStuff";
}
return super.getPreEstablishedRedirectUri();
}
};
details.setId("google-oauth-client");
details.setClientId("xxxxxxxxxxx");
details.setClientSecret("xxxxxxxx");
details.setAccessTokenUri("https://www.googleapis.com/oauth2/v4/token");
details.setUserAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
details.setTokenName("authorization_code");
details.setScope(Arrays.asList("https://mail.google.com/,https://www.googleapis.com/auth/gmail.modify"));
details.setPreEstablishedRedirectUri("http://localhost:8080/xxx-api-web/v2/gmail"); //TODO
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
details.setGrantType("authorization_code");
return details;
}
I'm having the same problem you are and I'm leaning toward your first theory of using ThreadLocal storage. Here is how I'll probably go about my solution:
Set values from ServletRequest in LocalThread storage by overriding methods in OAuth2ClientAuthenticationProcessingFilter:
attemptAuthentication
successfulAuthentication
unsuccessfulAuthentication
requiresAuthentication
Then translate the URI's within the OAuth2RestTemplate by overriding the followign methods:
createRequest
doExecute
appendQueryParameter
I'll probably have to make my own #Bean for the RestTemplate that has an injected #Service that will lookup the dynamic Shopify domain.
I'll post my solution if it works.
I'm using Spring Cloud OAuth, I'm using the official sample from https://github.com/spring-cloud-samples/sso/ with GitLab (https://about.gitlab.com/) as OAuth Provider.
The Problem is that GitLab sends a Token of the Type "bearer" and Spring Cloud SSO retrieves the token and sends a Header in the form of
Authorization bearer xxxxxxx
which is rejected by the GitLab Server cause according to the documentation it only accepts Tokens in the form of
Authorization Bearer xxxxxxx.
This is probably a bug in the GitLab Server but is there any way to work around this problem with Spring Cloud SSO.
Update 19.03.:
This is what I've tried in SsoApplication.java of the SpringCloud-sample.
#Autowired
private OAuth2RestTemplate oAuth2RestTemplate;
#PostConstruct
private void modifyOAuthRestTemplate() {
this.oAuth2RestTemplate.setAuthenticator(new OAuth2RequestAuthenticator() { // this line gets called
#Override
public void authenticate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext clientContext, ClientHttpRequest request) {
// this line is never called
}
});
}
instead of the newly Injected OAuth2ProtectedResourceDetails the "original" one gets called. Everytime I try to authenticate in the sample app
UPDATE: there is a callback in Spring Cloud if you define a bean of type UserInfoRestTemplateCustomizer you will get an instance of OAuth2RestTemplate during startup where you can apply customizations (e.g. an OAuth2RequestAuthenticator to change "Bearer" to "bearer").
The workaround I have seen others use is to #Autowired the OAuth2RestTemplate and modify its OAuth2RequestAuthenticator in a #PostConstruct (before it is used anyway).