We are trying to implement a SSO scheme using Azure AD and Spring security.
We found a few leads:
https://github.com/spring-guides/tut-spring-boot-oauth2
https://github.com/Pytry/azure-b2c-oauth2
But none of these tell the full story.
In fact we can't get passed the access token parsing, Spring has a different idea of what the JWT token should be.
Ideally we wouldn't want to write an SSO filter from scratch but override the Token Services to implement custom filtering for starters.
Has anyone successfully implemented this?
Any help would be appreciated.
Update: I found an easier method. Just add a resource parameter after the userAuthorizationUri.
security:
oauth2:
client:
...
userAuthorizationUri: https://login.microsoftonline.com/<<tenantId>>/oauth2/authorize?resource=https://graph.windows.net
...
https://stackoverflow.com/a/45828135/2231168
Original answer
At my office we found a foreign blog post which lead us to the final implementation http://statemachine.hatenablog.com/entry/2016/04/19/155920
As a workaround, you had to add two classes to capture OAuth2RestTemplate and request enhancers. It works with spring boot 1.3.8 which contains spring 4.2.8, we couldn't make it work with higher version.
application.yml:
azure:
resource: https://graph.windows.net
security:
oauth2:
client:
clientId: <<your client id>>
clientSecret: <<your client secret>>
accessTokenUri: https://login.microsoftonline.com/<<tenantId>>/oauth2/token
userAuthorizationUri: https://login.microsoftonline.com/<<tenantId>>/oauth2/authorize
clientAuthenticationScheme: form
scope: openid
resource:
userInfoUri: https://graph.windows.net/me?api-version=1.6
AzureRequestEnhancer:
#Component
public class AzureRequestEnhancer implements RequestEnhancer {
#Value("${azure.resource:null}")
private String aadResource;
#Override
public void enhance(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) {
if (!StringUtils.isEmpty(resource)) {
form.set("resource", aadResource);
}
}
}
AzureRequestEnhancerCustomizer:
#Component
public class AzureRequestEnhancerCustomizer {
#Autowired
private OAuth2RestTemplate userInfoRestTemplate;
#Autowired
private AzureRequestEnhancer azureRequestEnhancer;
#PostConstruct
public void testWiring() {
AuthorizationCodeAccessTokenProvider authorizationCodeAccessTokenProvider = new AuthorizationCodeAccessTokenProvider();
authorizationCodeAccessTokenProvider.setTokenRequestEnhancer(azureRequestEnhancer);
userInfoRestTemplate.setAccessTokenProvider(authorizationCodeAccessTokenProvider);
}
}
The PostConstruct annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization.
I hope this helps you with the implementation.
Related
I was trying to follow the same multitenancy setup as the official documentation, but with credentials in my keycloak.json file, but keycloak does not seem to authorize access token properly.
It appears to call the resolve function of my customized KeycloakConfigResolver multiple times. It does authorize the token at the first time it calls to resolve, but it then shows 'Failed to verify token' every time after that.
And at the end, it fails to verify my access token.
Using customized keycloakconfigresolver in realms with credentials result in 403 Forbidden error.
#Configuration
public class HeaderBasedConfigResolver implements KeycloakConfigResolver {
#Override
public KeycloakDeployment resolve(Request request) {
System.out.println("Start Header-based resolving");
String realm = request.getHeader("realm");
File file = new File("realm_json/" + realm + "-keycloak.json");
InputStream is = null;
try {
is = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
return KeycloakDeploymentBuilder.build(is);
}
}
And the following is the code to initialize the KeycloakConfigResolver bean.
#Bean
#ConditionalOnMissingBean(HeaderBasedConfigResolver.class)
public KeycloakConfigResolver keycloakConfigResolver() {
return new HeaderBasedConfigResolver();
}
But when I initialize the KeycloakConfigResolver Bean with the default Implementation KeycloakSpringBootConfigResolver, it authenticates normally. I wonder how I could implement multi-tenancy in keycloak using credentials
Keycloak adapters for spring are deprecated. Instead, use spring-boot-starter-oauth2-resource-server (to secure #RestController and #Controller with #ResponseBody) or spring-boot-starter-oauth2-client (to secure server-side rendered UI with Thymeleaf, JSF or whatever). If you don't know how, you can refer to Spring-boot official doc or to those tutorials I wrote.
To configure multi-tenancy with spring-boot-starter-oauth2-resource-server, you'll have to provide a custom JwtIssuerAuthenticationManagerResolver bean capable of resolving the right authnetication-manager based on the iss claim in the access-token:
http.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver))
In the same repo as the one holding the tutorials, I propose thin wrappers around spring-boot-starter-oauth2-resource-server. Those designed for JWT decoder support multi-tenancy definition from properties:
<!-- instead of keycloak adapters for Spring -->
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.8</version>
</dependency>
#EnableMethodSecurity
public static class SecurityConfig {
}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/realm1
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.issuers[1].location=https://localhost:8443/realms/realm2
com.c4-soft.springaddons.security.issuers[1].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/greet
com.c4-soft.springaddons.security.permit-all=/actuator/health/readiness,/actuator/health/liveness
I'm new to Spring boot and Spring Security. I have microservice project using Spring boot. And in my gateway app, I use OAuth2 for authentication. The authentication provider is from my organization and it is OIDC implementation.
I'm using oauth2 resource server to authenticate the bearer token, by configuring jwk-set-uri and jwk-set-uri properties.
spring-boot-starter-web => 2.6.7
spring-boot-starter-oauth2-resource-server => 2.6.7
spring-security => 5.6.3
application.properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://<org-auth-url>.com
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://<org-auth-url>/<jwk-uri>
With just above configuration, the authentication works fine. So I have not added any Security Config class also. But for authorization and other processing like to get user data in Controller, I need the user information and AD group details.
I have the user information endpoint URL. And when I test it in postman client, the response contains user information along with AD groups.
How to get the User details for Authorization?
Ok.
You've already added the required uri. Good.
Now you need to add some configuration:
#Configuration
#EnableWebSecurity
public class OAuth2ResourceServerSecurityConfiguration {
#Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
String jwkSetUri;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(HttpMethod.GET,
///// more your requestMatchers /////
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
}
}
Now you should be able to receive jwt claims in your controllers with #AuthenticationPrincipal annotation.
#RestController
public class YourController {
#GetMapping("/")
public String doAnything(#AuthenticationPrincipal Jwt jwt) {
return jwt.getSubject();
}
}
Please add more info and I'll try to explain it better :-)
==== UPD ====
Really useful official manual on this.
Official code samples
I am trying to familiarize myself with Spring Security, in particular migrating from Spring Security OAuth to Soring Security (as in the following example/guide https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide).
However, I am seeming to only get 403 Forbidden errors. I am accessing from Postman and am using my company's existing OAuth server. I am able to get a token from the auth server, so I know I have those credentials correct and I have verified what roles the OAuth user has.
I am using the following dependencies:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
This is the simple endpoint I am attempting to access:
#RestController
public class AppController
{
#GetMapping("/hello")
public String hello()
{
return "hello";
}
}
This is my application.yml file:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: <<company-website-here>>/uaa/oauth/token_keys
And this is my security configuration class:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/hello").hasRole("MY_ROLE")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
I can't seem to figure out why I seem to only get 403 errors. I have also tried adding #EnableWebSecurity to the security config class, but that didn't make a difference. Adding the auth server URL explicitly to the server and/or manually creating a JwtDecoder didn't do the trick either; it appears the url is being automatically picked up from the yml file, based on its property name.
I am trying to move away from using the org.springframework.security.oauth.boot dependency and ResourceServerConfigurerAdapter.
I had to add my own converter like so:
private static class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
{
private final Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter;
public JwtAuthenticationConverter()
{
this.jwtGrantedAuthoritiesConverter = jwt -> jwt
.getClaimAsStringList("authorities")
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
#Override
public final AbstractAuthenticationToken convert(#NonNull Jwt jwt)
{
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
return new JwtAuthenticationToken(jwt, authorities, jwt.getClaimAsString("client_id"));
}
}
Then had to add this to the main security config:
.jwtAuthenticationConverter(new JwtAuthenticationConverter());
There may be a couple of things happening.
As you're migrating to Spring Security 5, you may need to extract your authorities manually. Check this post and it's correct answer.
You are using hasRole function and this will append "ROLE_" before your authority/role. So if the role on your JWT token is not ROLE_JWT_ROLE you should use
hasTransaction.
I am facing issue with Token Relay while communicating from one ResourceServer to another ResourceServer.
My AuthServer is based on Dave Sayer's sample and this is the application.yml for resource server1.
security:
user:
password: none
oauth2:
client:
accessTokenUri: http://localhost:9999/uaa/oauth/token
userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
clientId: trusted
clientSecret: secret
The config is very similar in resource server2, except that it is using a different clientId
Here is how i am creating the OAuth2RestTemplate in resource server1.
#LoadBalanced
#Bean
#Autowired
public OAuth2RestTemplate loadBalancedOauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
This call requires JWT OAuth2 Token Relay, but its not happening probably.
#GetMapping("/test-relay")
public String fetchMyProfile2() {
final ResponseEntity<String> forEntity = oauthRestTemplate.getForEntity("http://my-oauth/users/me", String.class);
final String body = forEntity.getBody();
System.out.println("body = " + body);
return body;
}
This is the exception i get while invoking this endpoint /test-relay from Postman RestClient. I am specifying JWT Token in Authorization Header while making the call.
org.springframework.security.oauth2.client.resource.UserRedirectRequiredException: A redirect is required to get the users approval
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.getRedirectForAuthorization(AuthorizationCodeAccessTokenProvider.java:359)
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:205)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:121)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:648)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128)
I am using Spring Boot 1.5.2/3. My Resource Server is a UI Server as well, and this call works fine if i use Web Browser to hit the url.
UPDATE-1
This issue only happens for Resource Server that is a UI server too i.e. with #EnableOAuth2Sso annotation present on it. For a pure Resource Server that does not have #EnableOAuth2Sso, token relay works perfectly fine.
You might be affected by the bug I reported https://github.com/spring-cloud/spring-cloud-security/issues/123. See if this workaround helps :
#Configuration
public class WorkaroundConfig extends WebMvcConfigurerAdapter {
#Autowired
#Qualifier("tokenRelayRequestInterceptor")
HandlerInterceptor handlerInterceptor;
#Override
public void addInterceptors (InterceptorRegistry registry) {
registry.addInterceptor(handlerInterceptor);
}
}
In switching from Spring Cloud Brixton.M5 to Brixton.RC1 my ZuulProxy no longer passes Authorization headers downstream to my proxied services.
There's various actors in play in my setup, but most all of them are fairly simple:
- AuthorizationServer: runs separately; hands out JWTs to clients
- Clients: get JWTs from OAuth server; each with access to a subset of resources.
- ResourceServers: consume JWTs for access decisions
- MyZuulProxy: proxies various resource servers; should relay JWTs.
It should be noted that MyZuulProxy has no security dependencies whatsoever; It passed the Authorization: Bearer {JWT} header it receives to the ResourceServers, pre-RC1. MyZuulProxy is explicitly not a Client itself, and does not use #EnableOAuth2SSO or similar at the moment.
What could I do to get MyZuulProxy to relay the JWTs to the ResourceServers again when using Spring Cloud Brixton.RC1?
There's very little code to post: It's just #EnableZuulProxy, #EnableAuthorizationServer and #EnableResourceServer in three different jars. My Clients are not Spring applications.
Update: Fixed in https://github.com/spring-cloud/spring-cloud-netflix/pull/963/files
Sensitive headers can also be set globally setting zuul.sensitiveHeaders. If sensitiveHeaders is set on a route, this will override the global sensitiveHeaders setting.
So use:
# Pass Authorization header downstream
zuul:
sensitiveHeaders: Cookie,Set-Cookie
So pending a fix for https://github.com/spring-cloud/spring-cloud-netflix/issues/944, jebeaudet was kind enough to provide a workaround:
#Component
public class RelayTokenFilter extends ZuulFilter {
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// Alter ignored headers as per: https://gitter.im/spring-cloud/spring-cloud?at=56fea31f11ea211749c3ed22
Set<String> headers = (Set<String>) ctx.get("ignoredHeaders");
// We need our JWT tokens relayed to resource servers
headers.remove("authorization");
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 10000;
}
}
Set the sensitiveHeaders globally helped me solve the issue
zuul:
sensitiveHeaders: Cookie,Set-Cookie
Please note that the property name is sensitiveHeaders not sensitive-headers
[I use spring-cloud-starter-zuul version:1.3.1.RELEASE ]