When you access the relying party's url, you are redirected to the asserting party. I see in SAML-tracer:
<saml2p:AuthnRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
AssertionConsumerServiceURL="http://<host>/application>/login/saml2/sso/<id>"
...
The AssertionConsumerServiceURL is OK, except that it is not over HTTPS. How do I do that?
A part of my code in SecurityConfig is:
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation(assertingpartyMetadataUrl)
.registrationId("<id>")
.assertingPartyDetails((p) -> p.wantAuthnRequestsSigned(true))
.signingX509Credentials((c) -> c.add(Saml2X509Credential.signing(getRelyingPartyPrivateKey(), getRelyingPartyCertificate())))
I use spring-security 5.7.2 and opensaml 4.1.1.
Related
I'm having no luck in setting up a simple Spring gateway + oauth2 client with Keycloak standalone. The keycloack part of it works fine. Wireshark shows the token correctly generated.
The gateway security config is as follows. I'm still not sure whether there is a need to permitAll() the login callback url. Some guides suggest that it should be the case, others dont. I suspect the oauth provider manages that part behind the scenes. Nonetheless, with or without permitAll for the "/login/*" path, the result remains the same.
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(e -> e.anyExchange().authenticated());
http.oauth2Login(Customizer.withDefaults());
http.csrf().disable();
return http.build();
}
}
After login the redirect to https://localhost:9000/login seems incorrect, it should retry the original url, say https://localhost:9000/test-service/v1/listall/
EDIT
In order to rule out any misconfigurations, even tried a simplest possible gateway and api resource (un-authenticated) and setup simplest possible relam in keyclock. The results haven't changed :( There are dozens of articles out there doing the exact same thing.
Any pointers, ideas?
Many Thanks
Even I have completed all configurations including "creating a client", "creating a scope" and creating a "user" with this scope. I have encountered this issue again.
The only solution worked for me is adding scope :openid to application.yaml.
You can refer application.yaml Security OAuth config from here:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://${keycloak.base.url}/auth/realms/${realm}
client:
registration:
gateway:
provider: keycloak
client-id: ${client id}
client-secret:${client secret}
scope: openid
provider:
keycloak:
user-name-attribute: preferred_username
issuer-uri: https://${keycloak.base.url}/auth/realms/${realm}
According to offical doc(found at https://www.keycloak.org/docs/latest/securing_apps/#_spring_security_adapter), you should use
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
I figured it out, it was an incorrect user-name-attribute. The correct value is
user-name-attribute: preferred_username
For some reason, I had it set to preferred_name. It would save a lot of debug-time if only spring oauth writes the actual error instead of a generic invalid_grant.
First of all, for anyone troubleshooting Spring Security, I recommend enabling debug logging by setting the logging level in your application.properties or application.yml file.
application.properties format:
logging.level.org.springframework.security=DEBUG
application.yml format:
logging:
level:
org:
springframework:
security: DEBUG
I was having a similar issue when using OAuth2 along with Spring Session. Even after authenticating successfully with Keycloak, I would get this login error whenever my Spring Session had expired.
I do not condone messing around with authentication flows, but I was able to resolve this issue by setting my own authentication entry point on the ServerHttpSecurity:
http.exceptionHandling().authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/oauth2/authorization/keycloak"));
I then had to handle any requests to the "/login" page in my Controller. For me, I just redirect to my default landing page for a root request:
#Controller
#RequestMapping("/")
public class RootController {
#GetMapping({"", "login"})
public Mono<String> index() {
return Mono.just("redirect:/myLandingPage");
}
}
Somehow adding the "scope=openid" in my applications.properties works fine for me
spring.security.oauth2.client.registration.spring-cloud-client.scope=openid
Overview
I am trying to write a program that accesses a public REST API. In order for me to be able to consume it, I need to provide an OAuth2 token.
My App uses Spring Boot 2.4.2 and Spring Cloud version 2020.0.1. The app itself does call the REST API once every 24h, download the data, and stores it in a database. A different microservice consumes this data at some other point and needs the data to have been refreshed daily.
My approach to this is to use OpenFeign to declare the REST Client that consumes the REST API and provide it an OAuth2 token. This is a problem that is quite common, so I assume that machine to machine client_credentials workflow is well documented.
And indeed, I did find a simple example to do this with OpenFeign - here: https://github.com/netshoes/sample-feign-oauth2-interceptor/blob/master/src/main/java/com/sample/feign/oauth2/interceptor/OrderFeignClientConfiguration.java
TL;DR: Trying to write a machine-to-machine microservice requiring an OAuth2 token (client_credentials grant type).
Problem
This was my first try, but unfortunately with the new Spring Security release, I can't seem to get the OAuth2FeignRequestInterceptor instantiated, I might have a package problem. I then went on to study the documentation for Spring Security and the new OAuth2 rewrite, which can be found here: https://docs.spring.io/spring-security/site/docs/5.1.2.RELEASE/reference/htmlsingle/#oauth2client.
Approaches
My approach is to use a RequestInterceptor which injects the current OAuth2 token into the request of the OpenFeign client, by adding an Authorization Bearer header. My assumption is that I can retrieve this, more or less automagically, using the Spring Security OAuth2 layer.
Using the documentation I tried providing a bean of OAuth2RegisteredClient to my interceptor, as well as a bean of type OAuth2AccessToken - which both didn't work. My last try looked like this and is to be seen, as a sort of hail mary, kind of approach:
#Bean
public OAuth2AccessToken apiAccessToken(
#RegisteredOAuth2AuthorizedClient("MY_AWESOME_PROVIDER") OAuth2AuthorizedClient authorizedClient) {
return authorizedClient.getAccessToken();
}
This doesn't work because RegisteredOAuth2AuthorizedClient requires a user session, lest it is null. I also saw someone else on Stackoverflow trying the same approach, but they actually did it in a Controller (=> Resolving OAuth2AuthorizedClient as a Spring bean)
I also tried some approaches that I have found here on SO:
Feign and Spring Security 5 - Client Credentials (Provided answer uses Spring Boot 2.2.4 - thus not relevant anymore)
Alternative For OAuth2FeignRequestInterceptor as it is deprecated NOW another gentleman looking for an alternative for OAuth2FeignRequestInterceptor
OAuth2FeignRequestInterceptor class deprecated in Spring Boot 2.3 - solution here again required an active user-session
https://github.com/jgrandja/spring-security-oauth-5-2-migrate this Github repo pops up every now and then, I studied it, but I deem it irrelevant to my question - maybe I missed something? From what I understood, this sample application has multiple providers using multiple scopes - but still a user that triggers a login and thus the automagic generation of an OAuth2 token through Spring Security. (also featured in this question: Migrating from Spring Boot Oauth2 to Spring Security 5) [1]
https://github.com/spring-cloud/spring-cloud-openfeign/issues/417 -> as of right now there is no replacement for OAuth2FeignRequestInterceptor
My assumption is that I can somehow use Spring Security 5 to solve this, but I simply can't wrap my head around how to actually do it. It seems to me that most of the tutorials and code samples I have found actually require a user-session, or are outdated with Spring Security 5.
It really seems that I am missing something and I hope that somebody can point me in the right direction, towards a tutorial or written documentation on how to achieve this.
In depth example
I tried supplying an OAuth2AuthorizedClientManager as seen in this example (https://github.com/jgrandja/spring-security-oauth-5-2-migrate).
For this, I registered an OAuth2AuthorizedClientManager following the example code:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
and provided it for my RequestInterceptor as can be seen here:
#Bean
public RequestInterceptor requestInterceptor(OAuth2AuthorizedClientManager clientManager) {
return new OAuthRequestInterceptor(clientManager);
}
Finally I wrote the interceptor, which looks like this:
private String getAccessToken() {
OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId)
// .principal(appClientId) // if this is not set, I receive "principal cannot be null" (or empty)
.build();
return Optional.ofNullable(authorizedClientManager)
.map(clientManager -> clientManager.authorize(request))
.map(OAuth2AuthorizedClient::getAccessToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElseThrow(OAuth2AccessTokenRetrievalException::failureToRetrieve);
}
#Override
public void apply(RequestTemplate template) {
log.debug("FeignClientInterceptor -> apply CALLED");
String token = getAccessToken();
if (token != null) {
String bearerString = String.format("%s %s", BEARER, token);
template.header(HttpHeaders.AUTHORIZATION, bearerString);
log.debug("set the template header to this bearer string: {}", bearerString);
} else {
log.error("No bearer string.");
}
}
When I run the code, I can see "FeignClientInterceptor -> apply called" output in the console, followed by an Exception:
Caused by: java.lang.IllegalArgumentException: servletRequest cannot be null
My assumption is that I receive this, because I don't have an active user session. It seems to me thus, that I absolutely need one to fix this problem - which I don't have in machine-to-machine communcations.
This is a common use-case so I am sure I must have made a mistake at some point.
Used packages
Maybe I made a mistake with my packages?
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
According documentation need use AuthorizedClientServiceOAuth2AuthorizedClientManager instead of DefaultOAuth2AuthorizedClientManager
When operating outside of the context of a HttpServletRequest, use AuthorizedClientServiceOAuth2AuthorizedClientManager instead.
So. I was playing with your solution in my free time. And found the simple solution:
just add SecurityContextHolder.getContext().authentication principle to your code OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId).build();
Should be like this:
val request = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak") // <-- here your registered client from application.yaml
.principal(SecurityContextHolder.getContext().authentication)
.build()
Used packages:
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
application.yaml:
spring:
security:
oauth2:
client:
registration:
keycloak: # <--- It's your custom client. I am using keycloak
client-id: ${SECURITY_CLIENT_ID}
client-secret: ${SECURITY_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: openid # your scopes
provider:
keycloak: # <--- Here Registered my custom provider
authorization-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/authorize
token-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/token
feign:
compression:
request:
enabled: true
mime-types: application/json
response:
enabled: true
client.config.default:
connectTimeout: 1000
readTimeout: 60000
decode404: false
loggerLevel: ${LOG_LEVEL_FEIGN:basic}
SecurityConfiguration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration() : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
// #formatter:off
http
.authorizeRequests { authorizeRequests ->
authorizeRequests
.antMatchers(HttpMethod.GET, "/test").permitAll() // Here my public endpoint which do logic with secured client enpoint
.anyRequest().authenticated()
}.cors().configurationSource(corsConfigurationSource()).and()
.csrf().disable()
.cors().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.oauth2Client()
// #formatter:on
}
#Bean
fun authorizedClientManager(
clientRegistration: ClientRegistrationRepository?,
authorizedClient: OAuth2AuthorizedClientRepository?
): OAuth2AuthorizedClientManager? {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
.builder()
.clientCredentials()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(clientRegistration, authorizedClient)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
}
FeignClientConfiguration:
private val logger = KotlinLogging.logger {}
class FeignClientConfiguration(private val authorizedClientManager: OAuth2AuthorizedClientManager) {
#Bean
fun requestInterceptor(): RequestInterceptor = RequestInterceptor { template ->
if (template.headers()["Authorization"].isNullOrEmpty()) {
val accessToken = getAccessToken()
logger.debug { "ACCESS TOKEN TYPE: ${accessToken?.tokenType?.value}" }
logger.debug { "ACCESS TOKEN: ${accessToken?.tokenValue}" }
template.header("Authorization", "Bearer ${accessToken?.tokenValue}")
}
}
private fun getAccessToken(): OAuth2AccessToken? {
val request = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak") // <- Here you load your registered client
.principal(SecurityContextHolder.getContext().authentication)
.build()
return authorizedClientManager.authorize(request)?.accessToken
}
}
TestClient:
#FeignClient(
name = "test",
url = "http://localhost:8080",
configuration = [FeignClientConfiguration::class]
)
interface TestClient {
#GetMapping("/test")
fun test(): ResponseEntity<Void> // Here my secured resource server endpoint. Expect 204 status
}
I have a spring security based boot application for which I have configured an endpoint names /test for which I return a test.html page
#RequestMapping("/test")
public String test() {
return "test.html";
}
The endpoint works fine by itself, however if I set it as an OAuth2 failureUrl it becomes unavailable...
http.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.authorizeRequests()
.antMatchers("/test").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.failureUrl("/test");
Is this expected behavior? When I do this, spring security properly redirects to /test on authentication failure but /test is inaccessible and it ends up showing a generated page.
The behaviour of failureUrl differs based on whether or not a custom loginPage is configured.
Since you have not customized loginPage, the framework will intercept the failure URL ("/test") and generate the default error page, which is simply the default login page with an error message.
That is why you see the generated login page with accessing "/test".
Your Controller mapping for "/test" is ignored.
To tell the framework not to generate the error page, you can configure the failureHandler instead.
http
.oauth2Login((oauth2Login) -> oauth2Login
.failureHandler(new SimpleUrlAuthenticationFailureHandler("/test"))
);
Note: This may be confusing because the Javadoc for failureUrl does not describe its behaviour properly. I have created a GitHub issue in the Spring Security backlog to fix this.
I am trying to migrate spring security to latest version (spring security 5.2.2 and Cloud OAuth2). As #EnableOAuth2Client is in maintenance mode now, I am trying to use http.oauth2Login() with customization for success handler. Following is my security class -
#Configuration
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login().successHandler(new SimpleUrlAuthenticationSuccessHandler("<url to redirect>"));
}
}
I have registered client with name as 'custom'. Following is the flow in the browser -
http://localhost:9000/oauth2/authorization/custom -> IDP's login page and successful login -> Get Authorization code on URL http://localhost:9000/login/oauth2/code/custom -> it again goes to http://localhost:9000/oauth2/authorization/custom and infinite loop. Successhandler is not invoked. Also, I don't see access token generation in logs.
I have tried many things in last two days like tweaking security config etc but nothing works.
Any help is appreciated.
The problem was with user info endpoint. My IDP has not exposed user info endpoint while it is mandatory in spring oauth2 where the request was failing. I needed to override the default OAuth2UserService implementation to resolve the issue.
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.