Spring Boot OAuth 2 SSO how to extract token to pass back to thick client application - single-sign-on

I have a thick client application (C# but that should not matter).
All the users already exist in an authentication/authorization (3rd party) system that provides OAuth 2 API (authorize/access_token plus a user_info service).
I have a Spring Boot web service tier that will have RESTful web services that will be called by the thick client application that must only be called by authenticated users for protected web services.
To authenticate the thick client will launch a Web Browser (OS installed default) and will open https to restful.web.server:8443 /login of the Spring Boot web service tier. This will do the OAuth 2 (authorization_code) interaction. Once redirected back with a valid token I want to redirect to a custom URI passing the token and for the browser to close (if possible) so an OS registered application can extract the token and pass it via an IPC mechanism to the thick client application.
The thick client application can then pass the token to the Web Services in the header (Authorize: TOKEN_TYPE TOKEN_VALUE).
The Web Services must then validate the authenticity of the token.
The Web Services if called with an invalid token must just return an HTTP error and JSON error content (e.g. code+message) and not try and redirect to the login screen. This will be orchestrated by the thick client application.
I have no concern with any of the custom URI handling, IPC development, or thick client web service calls. It is all the Spring/SSO magic in getting the token to be sent to my thick client and returning the relevant error from protected web services without returning a redirect to the SSO login.
I appear to be authenticating and being sent a token but then I get an exception.
I have made some progress and it appears that by manually launching a browser and hitting my web service tier https to restful.web.server:8443 /login it redirects to the SSO site https to 3rdparty.sso.server /oauth/authorization (passing in client_id, redirect_uri, response_type=code, state). I can log in, and Spring is calling the https to 3rdparty.sso.server /oauth/access_token endpoint (I had to create a custom RequestEnhancer to add in Authorization: Basic ENCODED_CLIENT_ID_AND_CLIENT_SECRET to satisfy the access_token SSO API requirement).
This returns 200 OK but then I get exceptions and do not know how to extract the token. The access_token returned may not be using the standard property names but unsure when to go and check if this is the case. I done the authentication this way to keep the client id and client secret out of the thick client application and my web services must do the authorisation anyway. If there is a better way or pointers to someone else doing this already it would be greatly appreciated. I find so many examples that are either not quite relevant or more towards web applications.
server:
port: 8443
ssl:
key-store: classpath:keystore.p12
key-store-password: **********
keyStoreType: PKCS12
keyAlias: tomcat
servlet:
context-path: /
session:
cookie:
name: UISESSION
security:
basic:
enabled: false
oauth2:
client:
clientId: *******
clientSecret: *****************
accessTokenUri: https://3rdparty.sso.server/oauth2/access_token
userAuthorizationUri: https://3rdparty.sso.server/oauth2/authorize
authorizedGrantTypes: authorization_code,refresh_token
scope:
tokenName: accessToken
redirectUri: https://restful.web.server:8443/login
authenticationScheme: query
clientAuthenticationScheme: header
resource:
userInfoUri: https://3rdparty.sso.server/oauth2/userinfo
logging:
level:
org:
springframework: DEBUG
spring:
http:
logRequestDetails: true
logResponseDetails: true
#Configuration
#EnableOAuth2Sso
#Order(value=0)
public class ServiceConectWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// From the root '/' down...
.antMatcher("/**")
// requests are authorised...
.authorizeRequests()
// ...to these url's...
.antMatchers("/", "/login**", "/debug/**", "/webjars/**", "/error**")
// ...without security being applied...
.permitAll()
// ...any other requests...
.anyRequest()
// ...the user must be authenticated.
.authenticated()
.and()
.formLogin().disable()
.logout()
.logoutSuccessUrl("/login")
.permitAll()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
;
}
I expect that the secured web services would be accessible once authenticated via the browser whilst testing without the client and would not expect exceptions to be thrown. I need to be able to extract the returned token and pass it back to my thick client.
Redirects to 'https://3rdparty.sso.server/oauth2/authorize?client_id=***HIDDEN_CLIENT_ID***&redirect_uri=https://localhost:8443/login&response_type=code&state=***HIDDEN_STATE_1***'
Then FilterChainProxy : /login?code=***HIDDEN_CODE_1***&state=***HIDDEN_STATE_1*** at position 6 of 12 in additional filter chain;
Request is to process authentication
RestTemplate : HTTP POST https://3rdparty.sso.server/oauth2/access_token
RestTemplate : Response 200 OK
IllegalStateException: Access token provider returned a null access token, which is illegal according to the contract.
at OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:223) ```
Then end up at an error page
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
There was an unexpected error (type=Internal Server Error, status=500).
Access token provider returned a null access token, which is illegal according to the contract.

The access_token service was returning non-standard JSON names.
I created a MyOwnOAuth2AccessToken with the relevant non-standard JSON names the necessary de/serialisation classes.
I created a MyOauth2AccesTokenHttpMessageConverter class for returning my OAuth2AccessToken.
The MyOauth2AccesTokenHttpMessageConverter was plumbed in from an
#Configuration
public class ServiceConnectUserInfoRestTemplateFactory implements UserInfoRestTemplateFactory
within the
#Bean
#Override
public OAuth2RestTemplate getUserInfoRestTemplate()
method with the following code:
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
messageConverters.add(new ItisOAuth2AccessTokenHttpMessageConverter());
messageConverters.addAll((new RestTemplate()).getMessageConverters());
accessTokenProvider.setMessageConverters(messageConverters);
There is probably a better way to do this but this worked for me.

Related

APEX and Keycloak integration - redirect problem after login

I'm trying to upgrade my APEX application (APEX 20.2.0.00.20) with a Keycloak authorization procedure.
What I am doing:
A - On Keycloak:
Realm: we already have a realm configured, which is used by other apps, in other development technologies.
1 - Client configuration - first attempt:
Root URL: ${authBaseUrl}
Valid Redirect URIs: /realms/[realm]/[client]/*
Base URL: /realms/[realm]/[client]/
Web Origins: *
2 - Client configuration - second attempt:
Root URL: empty
Valid Redirect URIs: http://[APEX app IP]:7020/*
Base URL: http://[APEX app IP]:7020/ords/[workspace]/r/[app_name]/
Web Origins: *
APEX configuration:
Authentication Scheme:
Credential Store: [APEX webcredential configured to keycloak client]
Authentication Provider: generic oauth2 provider.
Authorization Endpoint URL:
https://[keycloak address]/auth/realms/[realm]/protocol/openid-connect/auth
Token Endpoint URL:
https://[keycloak address]/auth/realms/[realm]/protocol/openid-connect/token
User Info Endpoint URL:
https://[keycloak address]/auth/realms/[realm]/protocol/openid-connect/userinfo
Token Authentication Method: basic authentication and client id in body
Scope: email
Authentication URI Parameters: empty
Username: #sub# (#APEX_AUTH_NAME#)
Convert Username To Upper Case: no
Verify Attributes: yes
** Tests **
I run my APEX app URL in workspace.
Test result of Client configuration nr. 1:
The following URL is shown as result:
https://[keycloak host]/auth/realms/[realm]/protocol/openid-connect/auth?response_type=code&client_id=[client_name]&scope=email&redirect_uri=http://[APEX address]:7020/ords/apex_authentication.callback&state=[A TOKEN/HASH]
And on the screen, the keycloak background image with the message:
Invalid parameter: redirect_uri. and a return to application link.
Test result of Client configuration nr. 2:
The keycloak login URL is activated and the login form is shown.
I type my user and password (keycloak user, configured on the client) and submit.
The following URL is shown as result:
https://[keycloak host]:7020/ords/apex_authentication.callback?state=[A TOKEN/HASH]
And on the screen, a APEX grey background (I know is a apex screen because a error with the icon is shown here) with the message:
Error processing request.
Contact your application administrator.
Question:
I think the second configuration is better, because I can do the Login on keycloak, but the redirect by keycloak to APEX fails, I think I APEX side.
Maybe any information sent by keycloak is wrong or it is missed.
Anyone knows what is the right configuration in APEX and keycloak?

Keycloak authorization policy evaluation with spring cloud gateway

I am trying to use keycloak for authorization in spring cloud gateway. Keycloak does not provide any spring based adapters for policy enforcement for reactive stack.However, it does provide an endpoint for policy evaluation.
http://localhost:8080/realms/myrealm/protocol/openid-connect/token -- POST
Request:
grant_type:urn:ietf:params:oauth:grant-type:uma-ticket
response_mode:decision
audience:b2b
permission:spm_audit#GET
Header:
Authorization : bearer <JWT>
# spm_audit is the resource that I have created in keycloak and GET is the scope(using HTTP methods as api scopes).
RESPONSE:
{
"result": true
}
My problem is that above endpoint does not accept URI as permission in request body and I don't have any resource-name to request URL mapping at gateway.
One possible solution could be to use gateway's route id as resource name and pass it in permission
cloud:
gateway:
routes:
- id: spm_audit
uri: http://localhost:8001
predicates:
- Path=/gateway/spm/api/v1/registrations/{regUUID}/audit
filters:
- StripPrefix=1
metadata:
custom_scope: "test scope"
#Fetch the route info in auth manager
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); //(ServerWebExchange exchange)
route.getId();
The problem with this approch is that the route matching filters are applied after authorization filter and exchange.getAttribute(GATEWAY_ROUTE_ATTR) is coming as null, plus I will have to map all api paths in route configuration and will end up with a huge configuration file.
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, #Qualifier("keycloKWebClient")WebClient kycloakWebClient) {
http
.authorizeExchange()
.pathMatchers(
"/gateway/*/public/**")
.permitAll()
.and()
.authorizeExchange()
.anyExchange()
.access(keyalokAuthManager(kycloakWebClient))....#this is where I call policy evaluation api
https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_authorization_api
What about using spring-security for resource-servers with a JWT decoder? It would be far more efficient as it would save many round trips to authorization-server (JWT decoder validates access-token with authorization-server public key downloaded once when policy enforcer requires a call to authorization-server for each and every incoming "non public" request).
You can map Keycloak "roles" to spring "granted authorities" and apply Role Based Access Control either with:
http.authorizeExchange().pathMatchers("/protected-route/foo").hasAuthority("ROLE_IN_KEYCLOAK") in your Java conf
#PreAuthorize("hasAnyAuthority('ROLE_IN_KEYCLOAK')") on your components methods.
For that, all you have to do is provide with an authentication converter with custom authorities converter:
interface AuthenticationConverter extends Converter<Jwt, Mono<JwtAuthenticationToken>> {}
interface AuthoritiesConverter extends Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> {}
#Bean
AuthoritiesConverter authoritiesConverter() {
return claims -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
// concat client roles to following stream if your app uses client roles in addition to realm ones
return realmRoles.stream().map(SimpleGrantedAuthority::new).toList();
}
}
#Bean
public AuthenticationConverter authenticationConverter(AuthoritiesConverter authoritiesConverter) {
return jwt -> Mono.just(new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt.getClaims())));
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, AuthenticationConverter authenticationConverter) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(authenticationConverter);
}
You should also have a look at this repo: https://github.com/ch4mpy/spring-addons
There is a spring-addons-webflux-jwt-resource-server spring-boot (2.7 or later) starter which would save you quite some configuration hassle. Tutorials in this repo are using servlet variants of the libs, but the 4 starters (servlet/reactive with JWT-decoder/introspection) work the same and you should easily find what to adapt for your reactive app.

Springboot 2.7 with oidc bearer token always redirects to login page

I have a client Springboot app which needs to access an oidc-protected REST service, so no UI component or UI login. I have the following yaml in the client:
spring:
security:
oauth2:
client:
registration:
my-service:
client-id: client-id
client-secret: client-secret
authorization-grant-type: client_credentials
provider:
my-service:
token-uri: https://mytokenhost.mydomain/token
which points to a Keycloak server on which I have configured an oidc client with a service account and enabled it. I use a WebClient to connect to the REST service which is configured like:
#Bean
public WebClient webClient(final OAuth2AuthorizedClientManager authorizedClientManager) {
final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("my-service");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
I also needed to manually define an OAuth2AuthorizedClientManager for the WebClient to work:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
final ClientRegistrationRepository clientRegistrationRepository,
final OAuth2AuthorizedClientService authorizedClientService) {
final OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder().refreshToken().clientCredentials().build();
final AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
And then use the WebClient to make a call to the REST service:
final String s = webClient
.method(HttpMethod.GET)
.uri("http://localhost:8080/my-rest-service/service?param1=value")
.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId(
"catalogue-services"))
.retrieve()
.bodyToMono(String.class)
.block();
Via IntelliJ I can see I get a token back, however no matter what I do Springboot redirects to the login page of the REST service. The REST service has yaml:
spring:
security:
oauth2:
client:
registration:
my-service:
client-id: client-id
client-secret: my-secret
authorization-grant-type: client_credentials
provider:
my-service:
token-uri: https://mytokenhost.mydomain/token/openid-connect/token
and a security configuration in the REST service:
#Bean
public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
http.authorizeRequests(
a ->
a.antMatchers("/", "/error", "/someUrl")
.permitAll()
.anyRequest()
.authenticated())
.oauth2Login();
return http.build();
}
What is happening is that after successfully authenticating against KeyCloak, the REST service security filter is flagging the authentication as an anonymous login, presumably because the role is ROLE_ANONYMOUS and/or principal is anonymousUser. I can see the service account user name come back, the roles are included as well but maybe not being picked up. I have a realm role which is exposed in the token realm_access.roles and a client role which is exposed in resource_access.my-service.roles. When I debug the decision voting in the AffirmativeBase class I get:
AnonymousAuthenticationToken [Principal=anonymousUser,
Credentials=[PROTECTED], Authenticated=true,
Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null],
Granted Authorities=[ROLE_ANONYMOUS]]
I haven't used the Keycloak adapter since they look to be deprecated and the documentation still uses the WebSecurityConfigurerAdapter which is also deprecated in the Spring Security model.
It's likely I'm missing something really simple, but if anyone has done machine-to-machine oidc with Springboot and Keycloak and knows any tricks, any help would be appreciated.
If by "REST service" you mean a spring #RestControtroller (or #Controller with #ResponseBody), then it is an OAuth2 resource-server, not a client (like you configured in your "REST service" yaml file).
You can have a look at those tutorials which provide with OAuth2 concepts you need and sample configurations for resource-servers.

Keycloak frontend and backend clients

This is related to keycloak clients. My frontend is connected to public client and backend is connected to confidential client.
I am able to login, get the code, as I am using response_type=code by turning on "Standard Flow Enabled".
This code redirects and returns me Idtoken, refreshtoken and token.
Now I need to communicate with backend which is confidential, I would like to authenticate user using some of the values which I have received from the frontend client.
How can I do that?
Here is my frontend and backend conf
FRONTEND
{
"realm": "xyz",
"auth-server-url": "http://localhost:8333/auth/",
"ssl-required": "external",
"resource": "frontend-app",
"public-client": true,
"confidential-port": 0,
"enable-cors": true
}
BACKEND
keycloak.auth-server-url=http://localhost:8333/auth
keycloak.realm=xyz
keycloak.resource=backend-app
keycloak.principal-attribute=preferred_username
keycloak.bearer-only=true
keycloak.credentials.secret=xxx-xxx-xxx
this is from realm setting
This might help somebody.
My backend service which is springboot project with spring security keycloakAuthenticationProvider does authenticate the token received from the frontend public client.
Call from frontend
axios({
method: 'GET',
url: '/api/authenticate',
headers:{'Authorization': 'Bearer '+keycloak.token+''}
}).then((r) => {
console.log("response",r)
})
Call to backend
#GetMapping("/api/authenticate")
public ResponseEntity<SecureUserDto> authenticate() {
String username = SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();
User user = userRepository.findWithPrivilegesByUsername(username);
return ResponseEntity.ok();
}
But i still was not able to get it right on postman at ../token end point provided by keycloak server.
Anyways my work is done.

Spring Security authenticating RESTful web service

I'm working on adding basic authentication to my RESTful web service (implemented using Spring MVC) with Spring Security having never really used it before. Right now I'm simply using an in-memory UserService with the intention of adding a repository-based one later.
<security:http>
<security:http-basic />
<security:intercept-url pattern="/**" access="ROLE_ADMIN" />
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="admin" password="admin"
authorities="ROLE_USER, ROLE_ADMIN" />
<security:user name="guest" password="guest"
authorities="ROLE_GUEST" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
This works fine, i.e. sending the following request grants me access to the desired resource (where the encoded string is admin:admin):
GET /user/v1/Tyler HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
And sending the following request gives me an Error 403 (where the encoded string is guest:guest):
GET /user/v1/Tyler HTTP/1.1
Authorization: Basic Z3Vlc3Q6Z3Vlc3Q=
However, sending a request where the provided username is not contained in the UserService does not result in an Error 403 as I expected (or at least desired) but instead continues prompting for a username and password. E.g. (where the encoded string is user:user):
GET /user/v1/Tyler HTTP/1.1
Authorization: Basic dXNlcjp1c2Vy
Is there additional configuration required to respond with an Error 403 when unrecognized user credentials are provided? How can I go about doing that?
Firstly,
403 Forbidden should be used when user is already authenticated but is not authorized to perform particular action. In your example guest is successfully authenticated but is not granted permission to see page because he is just a guest.
You should use 401 Unauthorized to indicate that your user was not authenticated successfully.
More on HTTP errors codes: http://en.wikipedia.org/wiki/HTTP_401#4xx_Client_Error
Secondly,
you can specify your custom behavior by extending BasicAuthenticationFilter.
There is protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse AuthenticationException failed) method that you can override and do whatever is adequate. In default implementation that method is empty.
Spring Security docs on injecting custom filter: CLICK
Edit:
What Spring Security does each time your authentication input is invalid:
public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint {
...
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
So, the default behavior is correct. User is sent 401 and is asked to provide valid login/credentials.
Before overriding, try to understand the default behavior. Source code: CLICK
You should try this in a client like wget or curl. You browser nags you several times for Basic redentials if your are rejected with 401. This is probably the case here.