Looking at the new spring-security-oauth2-authorization-server library, I need to implement a custom authorization server for a resource server. Actually, I intend to manually implement the PRIVATE_KEY_JWT Authentication method that is either not yet implemented or lacking examples. I followed one good example that uses this approach (the authorization server issuing the access token to the resource server). The resource server has a simple configuration that points to my issuer (authorization server) as in the code:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://auth-server:9000
but this approach uses client-id and client-secret credentials like:
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
#Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client_id123")
.clientSecret("{noop}secret123")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("https://purl.imsglobal.org/spec/lti-ags/scope/score")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
I appreciate if anyone could refer me an example of either a PRIVATE_KEY_JWT implementation or a custom authorization implementation that receives a JWT and issues an access token.
In application.yml set up the location of the certs, and create a config class RsaKeyProperties to load them:
rsa:
privateKey: classpath:certs/private.pem
publicKey: classpath:certs/public.pem
Then in WebSecurityConfig:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
protected SecurityFilterChain configure(
BasicAuthFilter basicAuthFilter,
ActivitiAuthenticationFilter activitiAuthenticationFilter,
HttpSecurity http) throws Exception {
return http
// Disabling CSRF is safe for token-based API's
.csrf().disable()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeRequests(auth -> {
auth.antMatchers(
"/api/authenticate/**",
"/api/tenants/**").permitAll();
auth.antMatchers("/api/**").authenticated();
// When an exception is thrown, ErrorMvcAutoConfiguration sets stuff up so that /error is called
// internally using an anonymous user. Without this line, the call to /error fails with a 403 error
// because anonymous users would not be able to view the page.
auth.antMatchers("/error").anonymous();
})
.build();
}
#Bean
public JwtDecoder jwtDecoder(RsaKeyProperties rsaKeyProperties) {
return NimbusJwtDecoder.withPublicKey(rsaKeyProperties.publicKey()).build();
}
#Bean
public JwtEncoder jwtEncoder(RsaKeyProperties rsaKeyProperties) {
JWK jwk = new RSAKey.Builder(rsaKeyProperties.publicKey())
.privateKey(rsaKeyProperties.privateKey())
.build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
}
The "SCOPE_" prefix that the identity server library was adding in the JWT didn't work well with spring security as far as I could figure out, so I removed that:
/**
* For some reason the JwtGrantedAuthoritiesConverter defaults to adding the prefix "SCOPE_" to all
* the claims in the token, so we need to provide a JwtGrantedAuthoritiesConverter that doesn't do
* that and just passes them through.
*/
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter authConverter = new JwtAuthenticationConverter();
authConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return authConverter;
}
Related
I've looked at pretty much all of the blogs and SO posts on this topic and I'm not seeing a solution. I have a WebSecurityConfigurerAdapter that looks like this:
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
String issuerUri = "issuer url";
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers(HttpMethod.GET, "/test").hasRole("Task.Write")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(jwt ->
jwt.decoder(JwtDecoders.fromIssuerLocation(issuerUri))
)
);
}
I have a controller that looks like this:
#GetMapping(value="/test")
public ApplicationResponse test(#AuthenticationPrincipal Jwt jwt) {
// Map<String, Object> x = jwt.getClaims();
return new ApplicationResponse("ok", "ok");
}
However when I hit this endpoint with Postman with a valid JWT I get a 403 error. I have tried prefixing the role with ROLE_ and Role_ and tried numerous other things but it always is 403.
What's weird too is if I do a permitAll() instead of the authenticate and let the JWT go through. I can look into the JWT object in the controller. I see that the role is there. So why does this WebSecurityConfigurerAdapter always throw a 403 when the JWT is valid and the role is there?
I noticed that the roles are located in the claim in the JWT. Maybe I need to get it from here? I don't see how to get roles from claims in the configure method:
By default, Spring Security converts the items in the scope or scp claim and uses the SCOPE_ prefix. You can change both conventions by defining a custom JwtAuthenticationConverter bean.
To export authorities from a roles scope and use the ROLE_ prefix, you can define the following converter, so that you can use methods like hasRole("Task_Write").
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Small question regarding how to use Spring Security to specify which client certificate can access what specific predefined endpoint, please.
By predefined endpoint, I mean the web application has default endpoint (not those I defined via #RestController) such as the actuator endpoint /actuator/health, /actuator/prometheus, or the Spring Cloud Config endpoints, such as /config/myservice/ There is no possibility to #PreAuthorize.
I would like to just specify which client certificate can access which endpoint, such as:
Client certificate with UID=Alice can access /actuator/health and /config/myservice.
Client certificate with UID=Bob can access /actuator/prometheus
There are many examples online on, How to extract X509 certificate:
https://www.baeldung.com/x-509-authentication-in-spring-security
https://blog.codecentric.de/en/2018/08/x-509-client-certificates-with-spring-security/
But how to configure it in the application, i.e., this sort of mapping of which certificate can access what?
Thank you
#Setu gave you the essential information to solve the problem.
Please, consider the following code adapted from the one that can be found in one of the articles you cited:
#SpringBootApplication
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class X509AuthenticationServer extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// Define the mapping between the different endpoints
// and the corresponding user roles
.antMatchers("/actuator/health").hasRole("ACTUATOR_HEALTH")
.antMatchers("/actuator/prometheus").hasRole("ACTUATOR_PROMETEUS")
.antMatchers("/config/myservice").hasRole("CONFIG_MYSERVICE")
// Please, adjust the fallback as appropriate
.anyRequest().authenticated()
.and()
// Configure X509Configurer (https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/configurers/X509Configurer.html)
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(userDetailsService())
;
}
#Bean
public UserDetailsService userDetailsService() {
// Ideally this information will be obtained from a database or some
// configuration information
return new UserDetailsService() {
#Override
public UserDetails loadUserByUsername(String username) {
Objects.requireNonNull(username);
List<GrantedAuthority> authorities = null;
switch (username) {
// Match the different X509 certificate CNs. Maybe you can use the
// X509 certificate subject distinguished name to include the role in
// some way and obtain it directly with the subjectPrincipalRegex
case "Alice":
authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_ACTUATOR_HEALTH, ROLE_CONFIG_MYSERVICE");
break;
case "Bob":
authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_ACTUATOR_PROMETHEUS");
break;
default:
throw new UsernameNotFoundException(String.format("User '%s' not found!", username));
}
return new User(username, "", authorities);
}
};
}
}
Please, adapt the code as you need to meet your actual endpoints and users.
I expose to you my problem.
I have a webapp that call a REST API to find the cart of the client.
The application and the API are secured with spring security and a SSO Keycloak.
Actually, my webapp is functional, if my don't protect my api, all are ready.
But when i want to secure my api with role, i have every time an error 401 error: unauthorized. In fact is good, my api is secured, but the client that have role "USER" can't access to his cart.
When i attempt to take my bearer token to keycloak, is a good token (past in jwt.io). I attempted to use curl, but the result is same.
I use Feign in my webapp to call the API.
My Keycloak configuration in my api
#Configuration
#EnableWebSecurity
#ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true)
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public static class KeycloakConfigurationAdapter extends KeycloakWebSecurityConfigurerAdapter{
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakConfigResolver KeycloakConfigResolver(){
return new KeycloakSpringBootConfigResolver();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
.addFilterBefore(keycloakAuthenticationProcessingFilter(), X509AuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
.and()
.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/paniers/**").hasAuthority("USER")
.antMatchers("/commandes/**").hasRole("USER")
.anyRequest().permitAll();
}
}
My .properties for API
keycloak.auth-server-url=http://myserver:port/auth
keycloak.realm=wild_adventures
keycloak.resource=ms-commande
keycloak.credentials.secret=#######-####-####-####-###########
keycloak.bearer-only=true
My method in the controller of the api
#ApiOperation(value = "Récupère le panier avec la liste des évenements réservés ou renvoie un 404 NotFound")
#GetMapping(value = "paniers/{clientUuid}")
public Panier recupererPanier(#PathVariable(value = "clientUuid") String clientUuid) {
Panier panier = this.panierManager.getPanierByClientUuid(clientUuid);
if(panier == null)
throw new PanierInexistantException("Le panier n'a pas été trouvé");
return panier;
}
Thx for your help.
I noticed, that you mixed hasAuthority and hasRole methods in your security config. Should be either hasAuthority("ROLE_USER") or hasRole("USER"). Have you checked if "/commandes/**" resource works for you?
Ok after some research, i have find to reason of my problem.
First, Zuul secure the Authorization header of my request...
To solve that, i added this line in my properties "zuul.sensitiveHeaders: Cookie, Set-Cookie" (So, Zuul just secure Cookie and Set-Cookie).
Next, in my code i send the good token, but i needed to precise "Bearer " before my token string.
Maybe that solution can help someone :)
I have to integrate my system with third-party provider. This system is made with Spring and Angular.
Keep in mind that I need to create a custom login form instead redirecting to thirdy-party provider form like OAuth2.
He has created following endpoints:
Get token authentication
POST http://example.com/webapi/api/web/token
“username=972.344.780-00&password=123456&grant_type=password”
The response send me a token that I must use during all next requests.
Get user info
Authorization: Bearer V4SQRUucwbtxbt4lP2Ot_LpkpBUUAl5guvxAHXh7oJpyTCGcXVTT-yKbPrPDU9QII43RWt6zKcF5m0HAUSLSlrcyzOuJE7Bjgk48enIoawef5IyGhM_PUkMVmmdMg_1IdIb3Glipx88yZn3AWaneoWPIYI1yqZ9fYaxA-_QGP17Q-H2NZWCn2lfF57aHz8evrRXNt_tpOj_nPwwF5r86crEFoDTewmYhVREMQQjxo80
GET http://example.com/webapi/api/web/userInfo
That said, What I need to implement a custom authentication?
Could I use Spring OAuth2 in this case?
you can use Spring Security. The flow is the following. You authenticate against the Security token service. A cookie containing the authentication token is written to your browser. This token is sent on each subsequent request against the server.
On the rest server you will use Srping Security and more specifily you need to use AbstractPreAuthenticatedProcessingFilter in its implementation you will extract the token and associate it With the Security Context.
Here is example configuration of your spring Security
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
// TODO Auto-generated method stub
return super.authenticationManagerBean();
}
public void configure(WebSecurity web) throws Exception {
// do some configuration here
}
#Override
public void configure(HttpSecurity http) throws Exception {
// configure your Security here
// you can add your implementation of AbstractPreAuthenticatedProcessingFilter here
}
}
Here is your additional configuration
#Configuration
public class ExampleSpringSecurityConfig{
#Bean
public AuthenticationManager authenticationManager() {
return authentication -> authProvider().authenticate(authentication);
}
private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> userdetailsService() {
// Construct your AuthenticationUserDetailsService here
}
#Bean
public PreAuthenticatedAuthenticationProvider authProvider() {
PreAuthenticatedAuthenticationProvider authProvider = new PreAuthenticatedAuthenticationProvider();
authProvider.setPreAuthenticatedUserDetailsService(userdetailsService());
return authProvider;
}
}
Yes, you can use Spring Oauth2. You have to implement the Resource Owner Password Credentials Grant Oauth2 flow.
You have to create a login page for end user and your client app will send the user's credentials as well as your client system credentials (use HTTP Basic Authentication for client system credentials) to authorization server to get the token.
There are two ways to implement it-
Using client system id and password - When calling the token endpoint using the this grant type, you need to pass in the client ID and secret (using basic auth).
curl -u 972.344.780-00:123456 "http://example.com/webapi/api/web/token?grant_type=password&username=addEndUserNameHere&password=addEndUserPasswordHere"
Using Client system ID only (no client system password) - Authorization Server should have a client setup to support this flow without any password-
Child class of AuthorizationServerConfigurerAdapter should have below code-
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("clientId")
.authorizedGrantTypes("password")
.authorities("ROLE_CLIENT")
.scopes("read");
}
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
}
Now you can use below-
POST http://example.com/webapi/api/web/token?grant_type=password&client_id=my-trusted-client&scope=trust&username=addEndUserNameHere&password=addEndUserPasswordHere
Note - This flow is less secure than other Oauth2 flows and recommended for trusted client app only because user has to provide credentials to client app.
See here example
Using JWT with Spring Security OAuth2 with Angular
In this tutorial, we’ll discuss how to get our Spring Security OAuth2 implementation to make use of JSON Web Tokens.
http://www.baeldung.com/spring-security-oauth-jwt
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManager);
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
}
I have a REST service that uses OAuth2 authentication and that provides an endpoint to request a token with the client_credentials grant type. The application is based on Spring Boot.
So far I figured out I can request a token with something like:
#SpringBootApplication
#EnableOAuth2Client
public class App extends WebSecurityConfigurerAdapter {
#Autowired
OAuth2ClientContext oauth2ClientContext;
//...
#Override
protected void configure(HttpSecurity http) throws Exception {
// Does nothing - to allow unrestricted access
}
#Bean
protected OAuth2RestTemplate myTemplate() {
ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
details.setAccessTokenUri("http://localhost:8080/oauth/token");
details.setClientId("theClient");
details.setClientSecret("thePassword");
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
}
#RestController
public class TestController {
#Autowired
OAuth2RestTemplate myTemplate;
#RequestMapping("/token")
private String getToken() {
return myTemplate.getAccessToken().getValue();
}
}
And it almost works, but whenever I call the /token endpoint, there's an exception:
org.springframework.security.authentication.InsufficientAuthenticationException: Authentication is required to obtain an access token (anonymous not allowed)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:88) ~[spring-security-oauth2-2.0.9.RELEASE.jar:na]
at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221) ~[spring-security-oauth2-2.0.9.RELEASE.jar:na]
at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173) ~[spring-security-oauth2-2.0.9.RELEASE.jar:na]
...
The exception is thrown here, but I'm not sure how I can make Spring use context authentication other than AnonymousAuthenticationToken. In fact, I don't want any authentication from the client, because anonymous is perfectly okay. How can I achieve this?