[PLEASE SEE UPDATE SETION BELOW FOR LATEST ITERATION]
I am trying to set up a Spring Boot REST application using Spring Security. Our front-end is authenticating our users with OIDC in Okta, and is passing the JWT in the Authorization Bearer token. Okta is using PKCE.
I am referencing the following example, and it seems to be a very simple setup:
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html/boot-features-security-oauth2-resource-server.html
Our Spring Boot application is only going to be a Resource Server, so I think I only need to follow the directions in sections 2.1 to 2.2.3. I have #EnableResourceServer in my application, and I have the following in my application.yml:
spring:
security:
oauth2:
resource:
jwt:
key-set-uri: https://dcsg-hub-dev.okta.com/oauth2/v1/keysZ?client_id=???
When I call my app using Postman, I am getting a 401 error:
{
"error": "invalid_token",
"error_description": "Invalid access token: eyJraWQ..."
}
It shows the valid token, which I can verify by calling the Okta userinfo endpoint. Unfortunately, it does not give me any other information.
I ended up setting breakpoints in the org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter class. Stepping into the code called from this location, the error is being caused at code in org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
} else {
String token = (String)authentication.getPrincipal();
OAuth2Authentication auth = this.tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
Which calls the following code in the same class:
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
It seems that the code is using the token itself as a key to retrieve the token from a hash map. As you can see above, it is using getPrincipal() to get the 'token'. It seems to me it thinks the token should be a user ID, which it is trying to use to find the user's token. It seems odd that it is using the token as a key to get the value, when the value is already there.
Any ideas?
<<<<<<<<<<<<<< UPDATE >>>>>>>>>>>>>>>
I followed the doc in this link:
https://github.com/spring-projects/spring-security/tree/5.2.1.RELEASE/samples/boot/oauth2resourceserver-jwe
My application.yml now looks like this:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://dcsg-hub-dev.okta.com
jwk-set-uri: https://dcsg-hub-dev.okta.com/oauth2/v1/keys?client_id=
This is now definitely calling the JWK URI, whereas before it wasn't. However, now I am getting the following error:
Caused by: com.nimbusds.jose.proc.BadJOSEException: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
I set breakpoints in that code, and for whatever reason, the kid that comes back in the token/JWT from Okta does not match any of the keys that are in the Okta jwk-set-uri.
I found out from Okta that this feature is not supported, you need to create your own authorization server with API Access Management.
Related
Problem: My java springboot application receives JWT token from external system to authenticate a user with their external identity management provider which returns the user details upon success.
Once userdetails is received, the backend application must create a redirect url for the external system end user. The redirect url will land the user on my angular application to show the landing page.
Here on, all the rest api's should be allowed through an http session.
In case the user tries to access the rest api's directly, he should get an Authentication error.
How can we achieve authorization in this case since authentication was not done by my spring boot application. Can we create custom Spring session using spring security and manually put userDetails in the SecurityContext?
I am currently dealing JWT tokens obtained from Google. Including Google, pretty much all authorization servers provide rest APIs such as GET /userInfo, where you can carry the JWT token in the request header or in the URL as a GET parameter, and then verify if the JWT token is valid, non-expired, etc.
Because verifying a JWT token is usually stateless, these APIs generally come with a generous limit and you can call them as many times as you need.
I assume that you have Spring security integrated and then you can add a filter. In this way, every request has to be verified for its token in the header.
#Service
public class TokenAuthenticationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String header = request.getHeader("Authorization");
RestTemplate restTemplate = new RestTemplate(); // If you use Google SDK, xxx SDK, you do not have to use restTemplate
String userInfoUrl = "https://example.com/api/userInfo";
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", header);
HttpEntity entity = new HttpEntity(headers);
ResponseEntity<String> response = restTemplate.exchange(
userInfoUrl, HttpMethod.GET, entity, String.class, param);
User user = response.getBody(); // Get your response and figure out if the Token is valid.
// If the token is valid? Check it here....
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
}
We are implementing login with Keycloak (v11.0.3) and have been successful with keycloak login (username/password) and Github OAuth. Google Oauth proceeds as expected until the last step when we get a
We are sorry...
Unexpected error when authenticating with identity provider
The keycloak log/stack trace is below. I've checked the Google client/secret stuff and that is correct and have tested the google auth through python code successfully. One additional detail--the google client is in "test mode" and I have added our testing account to the list of acceptable credentials. Any suggestions on what might be going on? What additional troubleshooting steps can I take?
13:14:22,754 ERROR [org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider] (default task-46) Failed to make identity provider oauth callback: org.keycloak.broker.provider.IdentityBrokerException: Identity token does not contain hosted domain parameter.
at org.keycloak.keycloak-services#11.0.3//org.keycloak.social.google.GoogleIdentityProvider.validateToken(GoogleIdentityProvider.java:123)
at org.keycloak.keycloak-services#11.0.3//org.keycloak.broker.oidc.OIDCIdentityProvider.validateToken(OIDCIdentityProvider.java:536)
at org.keycloak.keycloak-services#11.0.3//org.keycloak.broker.oidc.OIDCIdentityProvider.getFederatedIdentity(OIDCIdentityProvider.java:364)
at org.keycloak.keycloak-services#11.0.3//org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider$Endpoint.authResponse(AbstractOAuth2IdentityProvider.java:472)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:138)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:543)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:432)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$0(ResourceMethodInvoker.java:393)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:395)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:364)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:150)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:110)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:141)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:104)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:440)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:229)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:135)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:138)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:215)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:245)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:61)
at org.jboss.resteasy.resteasy-jaxrs#3.12.1.Final//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
at javax.servlet.api#2.0.0.Final//javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
at org.keycloak.keycloak-wildfly-extensions#11.0.3//org.keycloak.provider.wildfly.WildFlyRequestFilter.lambda$doFilter$0(WildFlyRequestFilter.java:41)
at org.keycloak.keycloak-services#11.0.3//org.keycloak.services.filters.AbstractRequestFilter.filter(AbstractRequestFilter.java:43)
at org.keycloak.keycloak-wildfly-extensions#11.0.3//org.keycloak.provider.wildfly.WildFlyRequestFilter.doFilter(WildFlyRequestFilter.java:39)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
at io.undertow.core#2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at io.undertow.core#2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.core#2.1.3.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at io.undertow.core#2.1.3.Final//io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
at io.undertow.core#2.1.3.Final//io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
at io.undertow.core#2.1.3.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
at io.undertow.core#2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
at io.undertow.core#2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:68)
at io.undertow.core#2.1.3.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:269)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:78)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:133)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:130)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction.lambda$create$0(SecurityContextThreadSetupAction.java:105)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
at org.wildfly.extension.undertow#20.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1530)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:249)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:78)
at io.undertow.servlet#2.1.3.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:99)
at io.undertow.core#2.1.3.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:370)
at io.undertow.core#2.1.3.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
at org.jboss.threads#2.3.3.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at org.jboss.threads#2.3.3.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1982)
at org.jboss.threads#2.3.3.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at org.jboss.threads#2.3.3.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
at java.base/java.lang.Thread.run(Thread.java:834)
Looking at the keycloak code:
#Override
protected JsonWebToken validateToken(final String encodedToken, final boolean ignoreAudience) {
JsonWebToken token = super.validateToken(encodedToken, ignoreAudience);
String hostedDomain = ((GoogleIdentityProviderConfig) getConfig()).getHostedDomain();
if (hostedDomain == null) {
return token;
}
Object receivedHdParam = token.getOtherClaims().get(OIDC_PARAMETER_HOSTED_DOMAINS);
if (receivedHdParam == null) {
throw new IdentityBrokerException("Identity token does not contain hosted domain parameter.");
}
if (hostedDomain.equals("*") || hostedDomain.equals(receivedHdParam)) {
return token;
}
throw new IdentityBrokerException("Hosted domain does not match.");
}
It looks like that you have to inject into your token the claim "hd".
When you added Google as your Identify Provider, on the "Add Identify Provider" setting page you need to set the field Hosted Domain.
If hover over the Hosted Domain tool tip, you can read the following:
Set 'hd' query parameter when logging in with Google. Google will
list accounts only for this domain. Keycloak validates that the returned
identity token has a claim for this domain (...)
It is kind of silly that this field is not marked was mandatory, nevertheless Keycloak validates it.
I separate my application into 2 parts:
Front end : Vue js and connected with AWS congnito for login feature (email/pw or google social login).
Back end : Spring boot Restful. User information stored in database (a unique id from congnito as primary key.)
My flow of authentication
User redirected to congnito and login. congnito will return a unique id and JWT.
Front end passes the unique id and JWT to back end controller.
backend validate JWT and return user information from DB
My question is:
Is this a bad practice to authenticate on front end and pass data to back end for spring security? If so, may I have any suggestion to change my implementation flow?
To call AuthenticationProvider.authenticate, a Authentication consist username (in my case, the unique id from cognito) and password is needed (UsernamePasswordAuthenticationToken). Are there any implementation to set only username? or it is fine to set password as empty string?
// controller
public String login(HttpServletRequest req, String cognitoId, String jwt) {
// check JWT with AWS
if(!AwsJwtChecker(cognitoId, jwt))
return createErrorResponseJson("invalid jwt");
UsernamePasswordAuthenticationToken authReq
= new UsernamePasswordAuthenticationToken(cognitoId, "");
Authentication auth = authManager.authenticate(authReq);
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(auth);
HttpSession session = req.getSession(true);
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);
MyUser user = userRepository.selectUserByCognitoId(cognitoId);
return createLoginSuccessResponse(user);
}
// web config
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String cognitoId = authentication.getName();
// check user exist in db or not
MyUser user = userRepository.selectUserByCognitoId(cognitoId);
if (user != null) {
return new UsernamePasswordAuthenticationToken(username, "", user.getRoles());
} else {
throw new BadCredentialsException("Authentication failed");
}
}
#Override
public boolean supports(Class<?>aClass) {
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
}
Is this a bad practice to authenticate on front end and pass data to back end for spring security? If so, may I have any suggestion to change my implementation flow?
No, in fact it's best practice. JWT is exactly for that purpose: You can store information about the user and because of the signature of the token, you can be certain, that the information is trustworthy.
You don't describe what you are saving in the database, but from my perspective, you are mixing two authentication methods. While it's not forbidden, it might be unnecessary. Have you analysed your token with jwt.io? There are many information about the user within the token and more can be added.
Cognito is limited in some ways, like number of groups, but for a basic application it might be enough. It has a great API to manage users from within your application, like adding groups or settings properties.
You don't describe what you do with the information that is returned with 3). Vue can too use the information stored in the jwt to display a username or something like that. You can decode the token with the jwt-decode library, eg, and get an object with all information.
To call AuthenticationProvider.authenticate...
Having said that, my answer to your second question is: You don't need the whole authentication part in you login method.
// controller
public String login(HttpServletRequest req, String cognitoId, String jwt) {
// check JWT with AWS
if(!AwsJwtChecker(cognitoId, jwt))
return createErrorResponseJson("invalid jwt");
return userRepository.selectUserByCognitoId(cognitoId);
}
This should be completely enough, since you already validate the token. No need to authenticate the user again. When spring security is set up correctly, the jwt will be set in the SecurityContext automatically.
The problem I see with your implementation is that anyone could send a valid jwt and a random cognitoId and receive user information from the database. So it would be better to parse the jwt and use something from within the jwt, like username, as identifier in the database. The token can't be manipulated, otherwise the validation fails.
public String login(String jwt) {
// check JWT with AWS
if(!AwsJwtChecker(jwt))
return createErrorResponseJson("invalid jwt");
String identifier = getIdentifier(jwt);
return userRepository.selectUserByIdentifier(identifier);
}
I'm new to ServiceStack and using it to provide an endpoint that will receive incoming requests from a remote service. No end user is involved.
The authentication flow goes like this (as specified by the author of the remote service):
"Their" remote service calls "our" endpoint, with JWT in header
"Our" endpoint extracts the 'kid' from the JWT
"Our" endpoint calls "their" oauth endpoint, with 'kid' as parameter
"Their" oauth endpoint returns a public key in form of a JWK (RS256)
"Our" endpoint verifies the JWT signature using the JWK
Does ServiceStack support this authentication flow?
I think I need to write code that hooks into the request's authentication and does steps 2-5 above. Is that right?
Edit:
I found this answer which looks to be what I'm after, i.e. custom AuthProvider that overrides PreAuthenticate with steps 2-5 above.
using System;
using ServiceStack;
using ServiceStack.Auth;
using ServiceStack.Web;
namespace MyService
{
public class CustomJwtAuthProvider : AuthProvider, IAuthWithRequest
{
public CustomJwtAuthProvider ()
{
Provider = "CustomJwtAuthProvider";
AuthRealm = "/auth/CustomJwtAuthProvider";
}
public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null)
{
return session.IsAuthenticated;
}
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
{
throw new NotImplementedException("Authenticate() should not be called directly");
}
public void PreAuthenticate(IRequest req, IResponse res)
{
// Get kid from JWT
// Get public JWK from oauth endpoint
// Verify JWT signature using JWK
if ( /* JWT sig verified */ )
{
req.Items[Keywords.Session] = new AuthUserSession
{
IsAuthenticated = true,
};
}
}
}
}
Then in the ApplicationHost.Configure():
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new CustomJwtAuthProvider(),
}));
Does this approach seem right? Do I need to hand-roll the JWT authentication, or can I leverage ServiceStack's built in features and plugins more?
For them to be able to send you a JWT that ServiceStack accepts as an Authenticated Request, your App would need to be configured with either their AES Key if they're using HMAC-SHA* algorithm or their public RSA key if they're using JWE.
This flow is very strange, for them to be able to send you a custom JWT Key they would need to be able to craft their own JWT Key which means they need either the AES or private RSA Key your App is configured with where they'd be the only ones you will be able to authenticate with your App via JWT?
It's very unlikely that you'll want to configuring your App with that of a remote Service, instead you should probably use a JWT library like the JWT NuGet Package to just verify the JWT they send you is from them, then extract the KID, call their endpoint and validate the JWK they send you using a .NET library like JWK to verify their key.
Note this flow is independent from ServiceStack's JWT Support which you would use to enable stateless authentication to your Services via JWT. Here you're just using JWT and JWK libraries to verify their keys and extract required info from them.
Does Spring Security gives any such API where I can pass username & password and it will return either Authentication Object for successful authentication or AuthenticationCredentialsNotFoundException for unsuccessful authentication?
Let me elaborate my requirements:
Our application has a HTTP API(say, /createXXX.do) and the client is hitting this with username, password & other parameters.
Now I want to authenticate + authorize this access (coming from HTTP Hits to my application).
My planned design is like below:
a) I will not restrict access of my HTTP API context(i.e. /createXXX.do)
b) Once the request reached my doGet()/doPost(), I will retrieve the username & password from request and want to use some spring security API like below:
Authentication validateXXXXX(String username, String password)
throws AuthenticationCredentialsNotFoundException;
c) so that this above API internally push these username/password to the existing spring security chain and return me the Authentication Object for successful authentication or AuthenticationCredentialsNotFoundException for unsuccessful authentication.
d) For unsuccessful authentication, I will catch AuthenticationCredentialsNotFoundException and return the HttpServletResponse with AUTHENTICATION_ERROR code.
e) and for successful authetication, based on authiories from Authentication Object, I will allow or return the HttpServletResponse with AUTHORIZATION_ERROR code.
Can anyone know about such spring security API?
Any pointers/suggestion will be highly appreciated.
Thanks.
If you have just one authentication source (only LDAP or only DB) you can configure some implementation of org.springframework.security.authentication.AuthenticationProvider in your security context. Then you can use it:
User user = new User(login, password, true, true, true, true, new ArrayList<GrantedAuthority>());
Authentication auth = new UsernamePasswordAuthenticationToken(user, password,new ArrayList<GrantedAuthority>());
try {
auth = authenticationProvider.authenticate(auth);
} catch (BadCredentialsException e) {
throw new CustomBadCredentialsException(e.getMessage(), e);
}
// but your need to push authorization object manually
SecurityContext sc = new SecurityContextImpl();
sc.setAuthentication(auth);
SecurityContextHolder.setContext(sc);
It is "low level" manipulation. You can use element from Spring Security namespace. It can provide login controller, even login form for you (and it can handle this situation automatically).