Is it possible to get an access_token from Spring OAuth2 server without client secret? - spring

I am using Spring Security's OAuth2 server implementation. I am trying to get the access_token from the servers' /oauth/token endpoint using the OAuth2 "Password" grant type by only supplying username and password and the client id without the client secret.
This works fine as long as I provide the client id and the client secret in the Authorization header of my HTTP request like so:
curl -u clientid:clientsecret http://myhost ... -d "grant_type=password&username=user&password=pw&client_id=OAUTH_CLIENT"
Following the advice here: Spring OAuth2 disable HTTP Basic Auth for TokenEndpoint, I managed to disable HTTP Basic authentication for the /auth/token endpoint. But when I tried to get the access_token via cURL like so:
curl http://myhost ... -d "grant_type=password&username=user&password=pw&client_id=OAUTH_CLIENT"
I got a BadCredentialsException and could see the message:
Authentication failed: password does not match stored value
in my servers' log. At this point I was slightly irritated, because it was my understanding that this message only shows up when there's something wrong with the username and/or password, not the client id and/or secret. After additionally supplying the client secret in the cURL command like so:
curl http://myhost ... -d "grant_type=password&username=user&password=pw&client_id=OAUTH_CLIENT&client_secret=SECRET"
everything was fine again.
So does that mean I have to supply the client secret one way or another to access the /auth/token endpoint?
PS: I am aware of the fact that regarding security it is generally a good idea to protect this endpoint via HTTP Basic authentication, but there are some use cases where one would rather be able to do without.
Edit:
I seem to have found a way to omit the client secret. Here's my OAuth2 server configuration (notice the calls to allowFormAuthenticationForClients() and autoApprove(true)):
#Configuration
#EnableAuthorizationServer
class OAuth2Config extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
public OAuth2Config(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauth) throws Exception {
// allows access of /auth/token endpoint without HTTP Basic authentication
oauth.allowFormAuthenticationForClients();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("acme")
.autoApprove(true) // <- allows for client id only
.authorizedGrantTypes("authorization_code", "refresh_token", "password").scopes("openid");
}
}
Edit II:
The question here: Spring Security OAuth 2.0 - client secret always required for authorization code grant is very closely related to this one but deals with the OAuth2 grant type "Authorization Code", which results in a different workflow like the one you get with grant type "Password".

According to the specification (RFC 6749), if the client type of your application is public, a client secret is not required. On the contrary, if the client type is confidential, a client secret is required.
If Spring offers an API to set the client type, try to set the client type to public.

Spring Boot's implementation requires that a client-secret be passed in to authenticate. You can however override this by creating a bean of type AuthorizationServerConfigurer and configuring it yourself. This is the link to the documenation...

Use basic auth but leave the password empty.

In the implementation of AuthorizationServerConfigurerAdapter override configure and set password encoder to raw text encoder (do not use it as a default password encoder!).
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.passwordEncoder(plainTextPasswordEncoder())
.allowFormAuthenticationForClients();
}
private PasswordEncoder plainTextPasswordEncoder() {
return new PasswordEncoder() {
#Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return !StringUtils.hasText(encodedPassword) || passwordEncoder.matches(rawPassword, encodedPassword);
}
#Override
public String encode(CharSequence rawPassword) {
return passwordEncoder.encode(rawPassword);
}
};
}
}
Now, for OAuth client details (in memory or in a database), set the client secret to null. In this case, the client will be treated as public and will not require client_secret parameter. If you set client secret for OAuth client details (e.g. BCrypt hash), then the client will be treated as confidential. It will rely on default password encoder (e.g. BCrypt) and require client_secret parameter to be sent in order to obtain an access token.

Related

Spring boot with keycloak add api key auth for specific endpoint

in spring boot app I have:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true)
#Slf4j
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
List<String> aIPWhiteList;
#Autowired
List<String> bIPWhiteList;
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
String aIPAddressesFilterStr = defineIPFilters(aIPWhiteList);
String bIPAddressesPFilterStr = defineIPFiltersbIPWhiteList);
http.authorizeRequests()
.antMatchers("/order/a/**").access(aIPAddressesFilterStr)
.antMatchers("/b/order").access(bIPAddressesFilterStr)
.anyRequest().permitAll();
http.cors().and().csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
private String defineP4IPFilters(List<String> whiteList) {
StringBuilder ipAddressesFilterStr = new StringBuilder();
for (String ip: whiteList) {
ipAddressesFilterStr.append("hasIpAddress('").append(ip).append("') or ");
}
return ipAddressesFilterStr.substring(0, ipAddressesFilterStr.length() - 4);
}
}
I wonder how can I for this "b/order" make another auth, based on API Key stored in headers.
Basically only for this 1 endpoint I want authorize users differently.
Other endpoints are authorized by keycloak, are done from registered users.
But here I would like to auth it only by api key that is static.
any ideas ?
thanks!
I have two ideas which should save you quite some trouble, even if does not answer directly your question:
Do not use KeycloakWebSecurityConfigurerAdapter
It is part of the (very) deprecated Keycloak adapters for spring. Use spring-boot-starter-oauth2-resource-server instead. Refer to those tutorials for various ways to do it (with Keycloak)
Use OAuth2 client-credentials flow in place of API key
It serves that exact purpose: authenticate a trusted programmatic clients with "static" secrets.
With Keycloak, just declare "confidential" clients ("Client authentication" set to "On" and "Service Accounts Roles" enabled). Secret is to be retrieved from a "credentials" tab for this clients in Keycloak admin console. You can then define and assign different roles for each client if needed (such roles will appear in access-tokens, so you'll be able to use it in spring-security access control decisions)
Such clients will authorize their requests to resource-server(s) with access-tokens issued by your Keycloak instance just as other clients (used by humans) do. Only the protocol to get tokens (OAuth2 flow) differs: client-credentials for "robots" and authorization-code for "humans".
From the resource-server point of view, there will be absolutely no difference: all requests will be authorized with access-tokens issued by the same authorization-server => no need for a different authentication mechanism on some endpoints, just apply regular role-based access-control or wahtever else written with spring-security expressions.

How can I implement access tokens for a public Spring Boot API?

I have a public facing API created with Spring Boot. I want users of the API to include a token with requests so that I can ensure they are allowed to access it and also track how many requests are made within a certain period of time.
I'm a little confused on how to approach this because everything I am reading online is where a user sends their user/password and the application returns a token. This is not what I'm looking for.
I want something similar to when you use Google APIs or similar APIs where your token never changes unless you want it to change and you do not send Google API your user/pass on the first request.
Maybe I'm not using the correct terminology. Can someone point me in the right direction?
If you want to authenticate using a static token for each user you will have to create a custom AuthenticationManager that gets your request header and tries to match it aginst known keys.
Example using single key, you'd have to add a lookup to user table if needed
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
http.authorizeRequests(auth -> auth
.anyRequest().authenticated());
http.addFilter(tokenFilter());
http.csrf().disable();
return http.build();
}
public Filter tokenFilter()
{
AuthenticationTokenFilter filter = new AuthenticationTokenFilter(authenticationTokenHeader);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
protected AuthenticationManager authenticationManager()
{
final AuthenticationManager authenticationManager = new AuthenticationManager()
{
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
String principal = (String) authentication.getPrincipal();
if (!authenticationTokenValue.equals(principal))
{
throw new BadCredentialsException("The API key was not found or not the expected value.");
}
authentication.setAuthenticated(true);
return authentication;
}
};
return authenticationManager;
}
Do keep in mind that this approach is not the most secure, and if you'r application is public facing I would not recommend using this. And would recommend either using Authorization header with username/password or JWT
As a side note, I think you'r mistaken on how the google API authenticates. To the best of my knowledge all google APIs use Oauth2 for authentication, the static key/file you have does not provide access to the API it is only good for retrieving a short lived access token, in essence this would be not much different from standard JWT where you use some form of credentials to get a token with which you access the API.
for more info on JWT authentication:
https://www.baeldung.com/spring-security-oauth-jwt
https://blog.softtek.com/en/token-based-api-authentication-with-spring-and-jwt

Is possible ask for an acces token oauth2 just with refresh token in spring security? without basic authentication?

I would like to know if in spring oauth2 is possible get a new pair tokens (access token and refresh token) just using another refresh token, without the basic authentication (without clientId and clientSecret, is there any way?
For exemple:
WITH BASIC AUTH
curl -u clientId:clientSecret -X POST 'http://myapplication.oauth2/accounts/oauth/token?grant_type=refresh_token&client_id=<CLIENT_ID>&refresh_token=' -v
WITHOUT BASIC AUTH
curl -u -X POST 'http://myapplication.oauth2/accounts/oauth/token?grant_type=refresh_token&client_id=<CLIENT_ID>&refresh_token=' -v
I note that sprint BasicAuthenticationFilter in spring uses validation bellow, maybe override this filter and make the authentication just with refresh token.
String header = request.getHeader("Authorization");
if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}
The short answer is no. The class used to manage the Spring Oauth 2 endpoints is the following one:
#FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint
Both requests, I mean, get access token and refresh one use the same endpoint with different parameters. And the method to manage those ones is:
#RequestMapping(
value = {"/oauth/token"},
method = {RequestMethod.POST}
)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, #RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
} else {
String clientId = this.getClientId(principal);
...
As you can see, a Principal object is required (in this case provided by the Basic Authentication).
Even, if you configure the security of your project to permit that url without checking authentication, you will achieve to "enter" in above method but you will receive an InsufficientAuthenticationException because no Authentication instance has been provided.
Why custom authentication will not work
1. Create a custom AuthenticationProvider will not work because the method postAccessToken is invoked before. So you will receive an InsufficientAuthenticationException.
2. Create a OncePerRequestFilter and configure it to execute before process the current request:
#Override
protected void configure(HttpSecurity http) throws Exception {
http...
.anyRequest().authenticated()
.and()
.addFilterBefore(myCustomFilter, UsernamePasswordAuthenticationFilter.class);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(POST, "/accounts/oauth/**");
}
with a code "similar to":
#Component
public class CustomAuthenticationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
...
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("existingUser",
"passwordOfExistingUser",
Collections.emptyList()));
...
filterChain.doFilter(request, response);
}
The problem with this approach is the principal in TokenEndpoint comes from the HttpServletRequest not from Spring context, as you can see debugging BasicAuthenticationFilter class.
In your custom filter you can try, using reflection, set a value in userPrincipal property but, as you can verify, request has several "internal request properties" and that could be a "too tricky option".
In summary, Oauth standard needs user/pass to access to the resources, if you want to workaround in almost of provided endpoints maybe that project is not what you are looking for.
Workaround to include your own object in Spring Principal
I do not recommend that but if you still want to go ahead with this approach, there is a way to include your own value inside the principal parameter received by TokenEndpoint class.
It is important to take into account BasicAuthorizationFilter will be still executed, however you will be able to override the Spring principal object by your own one.
For this, we can reuse the previous CustomAuthenticationFilter but now your have to include the filters you need, I mean, allowed urls, parameters, etc You are going to "open the doors", so be careful about what you allow and not.
The difference in this case is, instead of add the configuration in our class that extends WebSecurityConfigurerAdapter we are going to do it in:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private CustomAuthenticationFilter customAuthenticationFilter;
...
#Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.checkTokenAccess("isAuthenticated()");
security.addTokenEndpointAuthenticationFilter(customAuthenticationFilter);
}
...

Spring Security OAuth 2.0 - client secret always required for authorization code grant

According to the spec, requests for a token using the authorization code grant are not required to be authenticated as long as the client_id is included in the request and the client_id is the same one used to generate the code. However, with the Spring Security OAuth 2.0 implementation, it appears that basic auth is always required on the /oauth/token endpoint even if the client was never assigned a secret.
It looks like there is support for allowing clients without a secret due to the isSecretRequired() method in the ClientDetails interface. What do I need to do to enable clients without a secret to be authenticated at the /oauth/token URL?
4.1.3. Access Token Request
The client makes a request to the token endpoint by sending the
following parameters using the "application/x-www-form-urlencoded"
format per Appendix B with a character encoding of UTF-8 in the HTTP
request entity-body:
grant_type
REQUIRED. Value MUST be set to "authorization_code".
code
REQUIRED. The authorization code received from the
authorization server.
redirect_uri
REQUIRED, if the "redirect_uri" parameter was included in the
authorization request as described in Section 4.1.1, and their
values MUST be identical.
client_id
REQUIRED, if the client is not authenticating with the
authorization server as described in Section 3.2.1.
If the client type is confidential or the client was issued client
credentials (or assigned other authentication requirements), the
client MUST authenticate with the authorization server as described
in Section 3.2.1.
Authenticating the client using the form parameters instead of basic auth is enabled using the allowFormAuthenticationForClients() method as shown in the code sample below.
class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
#Override
void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients()
}
}
The allowFormAuthenticationForClients() method triggers the addition of the ClientCredentialsTokenEndpointFilter which allows for authentication via form parameters.
Spring allows you to define OAuth2 clients with an empty secret.
These can be considered "public" clients, or clients that are unable to keep a secret. Think of Javascript apps, mobile apps, ..... You typically don't want to have client secrets stored there.
As you point out, according to the OAuth2 spec, a token endpoint can opt to not require a secret for these public clients.
So in Spring, simply define an OAuth2 client with an empty secret, and configure it for a limited set of grant types (authorization_code and refresh_token)
The Spring Security implementation token url will accept a token exchange without a client secret for that particular OAuth2 client.
In order to solve the issue see the method loadUserByUsername of class: org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService:
if (clientSecret == null || clientSecret.trim().length() == 0) {
clientSecret = this.emptyPassword;
}
Probably in your case emptyPassword has not been initialized with empty encoded password by your password encoder.
Set missing password encoder in AuthorizationServerConfigurerAdapter:
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.passwordEncoder(passwordEncoder);
}
This worked for me
#Override
public void configure(AuthorizationServerSecurityConfigurer cfg) throws Exception {
cfg
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients()
.passwordEncoder(clientPasswordEncoder());
}
#Bean("clientPasswordEncoder")
PasswordEncoder clientPasswordEncoder() {
return new BCryptPasswordEncoder(4);
}
Test 1:
Test 2:
Initially I had a similar setup to the accepted answer, which is definitely a prerequisite to make this work. But what is missing is that you cannot simply set the password to null. You must set it to an empty password, for example like this:
String secret = PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("");
clientDetails.setClientSecret(secret);
If you don't do this, you will still get a 401!
I use Spring boot 2.5.0 and I do not need the allowFormAuthenticationForClients(). My PasswordEncoder looks like this:
#Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence charSequence) {
return BCrypt.hashpw(charSequence.toString(), BCrypt.gensalt());
}
#Override
public boolean matches(CharSequence charSequence, String s) {
return BCrypt.checkpw(charSequence.toString(), s);
}
};
}
I generated the secret as empty string through this website
just click on the button "Bcrypt" and take the hash which you will insert into a database.
like this:
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client").secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("")) //empty
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://www.dev.com")
.scopes("all")
.autoApprove(true);
} #Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
It is ok.

Authentication Server for App Gateway and Web App sharing the user base with Spring Boot

I'm writing an App Gateway (REST-API) and a Web Application using Spring Boot. I've got a user database containing users and password hashes. Both applications will use the same user database, hence I want to have it in an isolated service. I've looked over https://spring.io/guides/tutorials/spring-security-and-angular-js/ which raised the idea to use an Auth Server via OAuth.
I'm quite unsure about the Request Flow for my use case. I think it will go App -> App Gateway (e.g. Login), which then does a request to /uaa/oauth/token with grant type password(since i got the user credentials). This request must contain client_id and client_secret and should return a token.
Is this concept correct for my usecase?
If so: How do I have to configure the Auth Server to use the Database instead of Basic Auth? All examples I found have a certain user / pw combination in application.properties
What would be the most idiomatic way to delegate Auth via username + pw to the Auth Server?
Edit: I found this to be very helpful: https://github.com/dsyer/sparklr-boot
Now I can do:
curl -H "Accept: application/json" my-trusted-client#localhost:8080/oauth/token -d grant_type=password -d username=user -d password=password
How do I wire some user backend into it? As far as I understand the following uses the default Spring Basic Auth settings.
application.yml:
security:
user:
password: password
Code:
#Configuration
#EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// #formatter:off
clients.inMemory().withClient("my-trusted-client")
.authorizedGrantTypes("password", "authorization_code",
"refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write", "trust").resourceIds("sparklr")
.accessTokenValiditySeconds(60).and()
.withClient("my-client-with-registered-redirect")
.authorizedGrantTypes("authorization_code").authorities("ROLE_CLIENT")
.scopes("read", "trust").resourceIds("sparklr")
.redirectUris("http://anywhere?key=value").and()
.withClient("my-client-with-secret")
.authorizedGrantTypes("client_credentials", "password")
.authorities("ROLE_CLIENT").scopes("read").resourceIds("sparklr")
.secret("secret");
// #formatter:on
}
}

Resources