I am seeing that myapp is able to process the OAuth2 JWT token properly on the server, but it is giving token conversion error on localhost.
My Flow is as below -
On Server, the myapp is behind our custom api-gateway
Getting Access Token - Through postman, I hit api-gateway token endpoint which in turn calls the authserver token endpoint. And I get OAuth JWT Token as response.
So, to summarize, postman
request: --(creds)--> api-gateway --(samecreds) --> auth-server
response: jwt token <-- (same jwt)-- api-gateway <--(jwt)-- auth-server
Next, I hit myapp endpoint - Again through postman, I hit api-gateway endpoints, which in turn hits the corresponding myapp endpoints. And I get the required response. For this request, before hitting the api-gateway, I set the header Authorization: Bearer JWT from step1
As the myapp developer, i know that the api-gateway is re-sending the JWT token to myapp using the same header mechanism i.e Authorization: Bearer JWT from step1. From the logs, i see this is the same value that I provided in postman when hitting the api-gateway
So, postman request: --(jwt)--> api-gateway --(same jwt)--> myapp
and response is some data, which is trivial for this discussion.
The decoded JWT Payload is as below:
{
"app-userId": "c54a-4140-9fa0-0f39",
"user_name": "abc#xyz.com",
"scope": [
"all"
],
"exp": 1656929583,
"authorities": [
"app1_viewer",
"app1_modifier",
"app2_viewer",
"app2_blog_creator],
"client_id": "api-gw-client"
...
}
Plz note - "client_id": "api-gw-client" field in the above payload. So the auth-server is issuing the token to api-gateway client.
Now on mylocal dev env i.e myapp running on localhost - i am trying to achieve similar flow as on server.
On localhost, the myapp is NOT running behind api-gateway, but its hit directly
Getting Access Token - Through postman, I hit api-gateway (same server instance as in server flow above. i.e api-gateway/auth-server's are not running on localhost but are running on server) token endpoint which in turn calls the authserver token endpoint. And I get OAuth JWT Token as response.
So, postman request: --(creds)--> api-gateway --(samecreds) --> auth-server
response: jwt token <-- (same jwt)-- api-gateway <--(jwt)-- auth-server
Yes, this token can be used in Server Flow Step 2 and it works. And the decoded JWT token payload is same as I posted earlier i.e
{
"app-userId": "c54a-4140-9fa0-0f39",
"user_name": "abc#xyz.com",
"scope": [
"all"
],
"exp": 1656929583,
"authorities": [
"app1_viewer",
"app1_modifier",
"app2_viewer",
"app2_blog_creator],
"client_id": "api-gw-client"
...
}
Plz note again - "client_id": "api-gw-client" field in the above payload. So the auth-server is issuing the token to api-gateway client. I am not sure yet if this field is trivial or important.
Next, I hit myapp localhost endpoint - Again through postman, But I hit myapp endpoints directly (not through api-gateway running on localhost). And for this request, before hitting, I set the header Authorization: Bearer JWT from step1.
BUT THIS TIME I GET ERROR:
p.a.OAuth2AuthenticationProcessingFilter : Authentication request
failed: error="invalid_token", error_description="Cannot convert
access token to JSON"
I want to know what is causing this error. I don't think the client_id: api-gw-client is causing this. Anyhow, I created a signed jwt token with client_id: myapp and used it in request. But still I get same error.
The key I am using on localhost matches(corresponds) to the one that the auth-server is using to sign. I double checked. So its for sure not a key issue.
I need the localhost based setup working, so i can test my api's locally with out deploying to server(deploying to server is quite time consuming in my case). So this setup is quite important for me to meet my deadlines. Any answers/suggests are greatly appreciated.
The Spring OAuth2 Libraries used in my project are as below -
org.springframework.security:spring-security-oauth2-jose:5.4.2
org.springframework.cloud:spring-cloud-starter-oauth2:2.1.3.RELEASE
And the class which is giving error is: OAuth2AuthenticationProcessingFilter.java (API Doc)
Sorry, I won't answer your question (#Toerktumlare is right, your security configuration is missing) but will instead try to explain why I would not make the API gateway an OAuth2 client.
Simply put, the role of an API gateway is to be a black box for a system resources, which could make it seen as a resource-server, but it should, in my opinion, remain transparent to OAuth2 (and other authentication mechanisms). Keep things simple:
clients (UI) handle user login (when needed)
resource-servers control access to resources
gateway (if any) should remain as transparent as possible, just offering a single entry-point for all resources.
In your scneario:
how do you expect things to happen if you're asked to implement Multi Factor Authentication: in addition to login / password, require one of biometry, external app validation, Goolgle authenticator token or whatever? Do you really intend to implement all that in the gateway in addition to the authorization-server?
if it always get api-gw-client as client ID, how can your authorization-server achieve client based processing (like checking requested scopes are legit for a client, or adding client authorities to the access-tokens)?
what about multi-tenant scenarios (R1 and R2 resource-servers behind the gateway expect identities from different issuers, or C1 and C2 clients do not authenticate users against the same authorization-server)?
what if some clients and resource-servers use something else than OAuth2?
Also, your client(s) must know what media-type is accepted and produced at each API endpoint (XML, JSON, PDF, multipart, etc.) and set Content-type and Accept headers accordingly. Why would it be different for Authorization header?
In my opinion, if you want to save time and energy, use your API gateway to proxy your resource-servers only. Leave authorization-server(s) appart. Same for external APIs you do not maintain but your client(s) need, if any (Tweeter feed, Google API, etc.).
This is how I always configure my resource-servers:
Authorization header is missing or invalid (malformed, expired, wrong issuer, bad audience, etc.) => 401 (unauthorized) and not 302 (redirect to login)
in multi-tenant scenarios, why should resource-server bother figuring out which authorization-server to redirect to ?
What should be the meaning of a redirect for clients which are not running in a browser, like mobile apps?
Authorization header is valid but associated claims fail to pass access-control (bad authorities or not the expected subject for instance) => 403 (forbidden)
Clients should know how to aquire required access-token(s) and how to set Authorization header when it sends a request (this is what you curently do with Postman). It can even intercept 401 to trigger user authentication and then retry failed resource access. Serious OAuth2 client-side librairies (like angular-auth-oidc-client for Angular) provide with such features.
With such clients, gateway can act as facade for your resource-servers (and authorization-server or external APIs if you like, but why?), forwarding Authorization header and completely ignoring users login.
Related
As we can see here: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-par#section-2.2
pushed authorization request lifetime should be between 5 and 600 seconds.
So assume that it's 60 seconds. Then client redirects user to authorization endpoint and... user is not logged in. So authorization endpoint redirect the user to endpoint with login page with request_uri as query param. The user logs in, login page redirect user to authorization endpoint with request_uri from query param. Probably, if lifetime was about 10s the request_uri is expired now (and what's more it's used more than once). So how can we handle the flow when user is not authenticated while he is redirected to authorization endpoint and we use PAR?
I know that can be 600 seconds also, but the recommendations say that this lifetime should be as short as possible. Therefore, it seems to me that I misunderstand how PAR works. I don't think even 10 minutes is enough because what if user currently doesn't have account at the identity provider or there is MFA used?
Please tell me, how PAR really works.
Consider a browser based app implementing this flow:
STEP 1
The browser wants to begin a login and calls its backend. The backend sends a standard Open ID Connect request like this to the authorization server:
POST https://login.example.com/oauth/authorize/par
Authorization: Basic czZCaFo3RmpmcDBa:QnIxS3REUmJuZbUl3
client_id=myclient&
redirect_uri=http%3A%2F%2Fwww.example.com%2F&
scope=openid%20profile&
response_type=code&
response_mode=jwt&
code_challenge=WQ4Y4CQpO8W6VtELopzYHdNg&
code_challenge_method=S256&
state=NFBljlVuB1GDjgGARmqDcxtHhV8
The authorization server saves the OIDC request details. Note also that the authorization header includes the client secret, which is one of the key features of PAR - the client authenticates before the redirect to the authorization server. So a malicious app cannot redirect a user with your client ID, since they do not know the secret.
STEP 2
The browser then uses the request URI. The short time you mention is only for this redirect and does not impact user login time:
https://login.example.com/oauth/authorize?
client_id=myclient&
request_uri=urn:ietf:params:oauth:request_uri:7d353fc8-9b94-488f-8c61-cf7cc1dfef9e"
STEP 3
The user logs in, and in some cases that might take a minute or so, as you say. Then a response is returned to the browser:
https://www.example.com/callback?response=eyJra...
In this example I am receiving the response as a JWT, using a related standard called JARM that can be used in conjunction with PAR. The JWT looks like this and could contain an error response in some cases:
{
"exp": 1629112321,
"iss": "https://login.example.com",
"aud": "myclient",
"iat": 1629112301,
"purpose": "authz_response",
"code": "abcdef",
"state": "12345abcdef"
}
If you don't use JARM you will instead receive code, state and error fields in the browser URL. The flow finishes with the usual authorization code grant POST, to swap the code for tokens.
SUMMARY
All of the above is designed to prevent man in the browser attacks. Eg a malicious party cannot alter any fields in flight. Extra security is therefore added to the standard code flow.
I'm having trouble to allow users to logout from an application that uses Keycloak for access management.
I have found this topic being discussed here and there, but not clear instructions on how to handle the logout.
I tried to cause the logout of an user redirecting the browser to an endpoint of the following format:
https://example.com/auth/realms/myrealm/protocol/openid-connect/logout?id_token_hint=mytoken&post_logout_redirect_uri=https://example.com/initialpage/
What I used as "mytoken" was the access_token I had obtained making a post request to the endpoint:
https://example.com/auth/realms/playipintern/protocol/openid-connect/token
passing to it parameters like the ones bellow:
grant_type="authorization_code"
code=code_obtained_from_a_url_to_which_keycloak_redirected_the_browser
client_id=client_id_created_using_key_cloak_gui
redirect_uri=the_to_which_keycloak_redirected_the_browser
and reading the body of the response. The content of the body was a json, like the one bellow:
{
'access_token': 'long_token_I_used_latter_as_token_hint_trying_to_logout',
'expires_in': 300,
'refresh_expires_in': 1800,
'refresh_token': 'other_long_token',
'token_type': 'bearer',
'not-before-policy': 0,
'session_state': 'a_shorter_code',
'scope': 'email profile'
}
My logout attempt resulted in the following message in Keycloaks log:
22:53:51,686 WARN [org.keycloak.events] (default task-24) type=LOGOUT_ERROR, realmId=playipintern, clientId=null, userId=null, ipAddress=192.168.16.1, error=invalid_token
and the response said "We are sorry, session not active".
Now I'm aware that I should have used the id_token and not the access_token to logout, but received no id_token in the json.
Somewhere, someone said I should have included
scope=openid
in the parameters that I used to obtain the token. I did it, expecting to find an "id_token" field in the json, but nothing changed.
Someone else reported to have needed to create a scope (I believe using Keycloak's GUI) named "openid" to obtain the token. That didn't make much sense to me, but I tried it anyway and added the just created scope to the client scopes using Keycloak's GUI again. Oncemore, the json didn't change.
I tried to use the refresh_token as the id_token, but that also resulted in an invalid token message.
I don't know what to try now. Any help is appreciated.
Thank you.
/token endpoint returns only the access token by default. No refresh token is returned and no user session is created on the Keycloak side upon successful authentication by default. Due to the lack of refresh token, re-authentication is required when the access token expires. However, this situation does not mean any additional overhead for the Keycloak server because sessions are not created by default.
In this situation, logout is unnecessary. However, issued access tokens can be revoked by sending requests to the OAuth2 Revocation Endpoint as described in the OpenID Connect Endpoints section:
/realms/{realm-name}/protocol/openid-connect/revoke
Example:
POST /revoke HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
token=45ghiukldjahdnhzdauz&token_type_hint=access_token
You need to put your token in place of 45ghiukldjahdnhzdauz.
token_type_hint can take either access_token or refresh_token as value to define which type of token you want to revoke.
You will have to add scope=openid to your initial request to http://example.com/auth/realms/playipintern/protocol/openid-connect/auth (note the /auth instead of /token at the end) before the redirect from where you copied the access code.
You can find further information and explanation in this article.
when I send a valid request from postman to spring boot app that integrated with Keycloak, It works perfect and send 200 seccess code. If I send the request without Authorization bearer header for second time, It send 200 success code again. However, It must send 401 error code. In fact, until the token is valid, resource server does not check Authorization header. I encounter this bug when I use spring security config class.
I'm using the OAuth Authorization Code flow to authenticate the user and authorize my application against the WSO2 Identity Server. I'm using a simple node/express server, with Passport.js, to get the Access Token, and Postman to use that Access Token to make a few test requests to the SOAP APIs.
When using a Bearer Token method to authorize my application, I get the following error in the IS logs: 0 active authenticators registered in the system. The system should have at least 1 active authenticator service registered. I get the following error in Postman: 500 Internal Server Error, with the following response body, <faultstring>Authentication failure</faultstring>.
Here is what it looks like in Postman:
The same Access Token works with a REST API request, like "https://localhost:9443/scim2/Me".
Can anyone tell me what I'm missing here?
SOAP APIs in WSO2 Identity Server cannot be authenticated with Bearer tokens. They can be authenticated with Basic authentication and cookies. That's the reason for getting Authentication failure in the response.
But REST APIs in the Identity Server can be authenticated with Bearer tokens. So /scim2/Me authenticate successfully with access token.
Try to get the Access token manually from Authorize service and use it
Step 1: Get authorization code
https://<is_server_url>:9443/oauth2/authorize?client_id=<id>&redirect_uri=<callback_url>&response_type=code&scope=openid
You will get an authorization code on the callback URL
Step 2: Call token service to get access token
Post https://<is_server_url>:9443/oauth2/token
Content-Type:application/x-www-form-urlencoded
Authorization:Basic <base64encoded "<client_id>:<client_secret>">
grant_type:authorization_code
scope:openid
code:<code_from_step_1>
redirect_uri:<callback_url>
exp:
client_id=**abcdefgh12345678**
client_secret=**xyzsecretkey**
callback_url=**http://locahost/callback**
scope=openid
server: localhost
base64encode(client_id:client_secret)= base64encode(abcdefgh12345678:xyzsecretkey) => YWJjZGVmZ2gxMjM0NTY3ODp4eXpzZWNyZXRrZXk=
GET https://localhost:9443/oauth2/authorize?client_id=**abcdefgh12345678**&redirect_uri=**http://locahost/callback**&response_type=code&scope=openid
it will make a request back to the callback url with a parameter code, lets say code=this01is02your03code, please check your browser address bar
POST https://localhost:9443/oauth2/token
HEADERS
Content-Type:application/x-www-form-urlencoded
Authorization:Basic **YWJjZGVmZ2gxMjM0NTY3ODp4eXpzZWNyZXRrZXk=**
BODY
grant_type:authorization_code
scope:openid
code:this01is02your03code
redirect_uri:http://locahost/callback
this will return an access token, let say token returned by the server is 12345678ASDFGH
Now you could use this token to call any RestFull or SOAP service
Authorization: Bearer 12345678ASDFGH
I am writing an SSO provider for MS Graph APIs Azure AD v2 endpoint leveraging Spring OAuth2.
I am progressing with the implementation and constant testing but I stumbled upon an error returned by AAD which is puzzling me. After all, this should all be plain standard OAuth 2 flow.
I successfully configured my application on MS dev portal, providing a localhost redirect URL (which, for the record, is the only supporting the http scheme. Kudos to MS). So when I invoke http://localhost/myapp/auth/office365 Spring security successfully intercepts the invocation, provides a correct redirect to my browser with client ID to https://login.microsoftonline.com/common/oauth2/v2.0/authorize with expected parameters.
Microsoft shows a consent screen to me, after which I get redirected back to my Spring Security application via HTTP GET with expected authorization code parameter.
The problem is that when the application tries to negotiate the given authorization code for a bearer token headaches start. Spring Security invokes a POST to https://login.microsoftonline.com/common/oauth2/v2.0/token but ends in 401 error.
Here is the stack trace
error="invalid_request", error_description="AADSTS90014: The request body must contain the following parameter: 'client_id'.
Trace ID: 9acd2a10-1cfb-443f-9c57-78d608c00c00
Correlation ID: bf063914-8926-4e8f-b102-7522d0e3b0af
Timestamp: 2017-10-09 15:51:44Z", correlation_id="bf063914-8926-4e8f-b102-7522d0e3b0af", error_codes="[90014]", timestamp="2017-10-09 15:51:44Z", trace_id="9acd2a10-1cfb-443f-9c57-78d608c00c00"
at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:100)
at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:33)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3072)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:235)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal(AbstractJackson2HttpMessageConverter.java:215)
at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:193)
at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport$AccessTokenErrorHandler.handleError(OAuth2AccessTokenSupport.java:235)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:621)
at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:137)
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:209)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:121)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173)
at org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:105)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
I have looked into Spring security implementation to find the cause,.
It happens that the error message error="invalid_request", error_description="AADSTS90014: The request body must contain the following parameter: 'client_id'. is self explanatory: MS Graph wants the client ID (which is still supplied by the basic authentication header) in the request body. Stop for a moment. I want to use plain old Spring Security and not third-party specific jars in order not to pollute my classpath.
Looking into Java source code of Spring OAuth 2 the problem is damn clear. Spring uses the client ID only in getParametersForAuthorizeRequest, which is used to generate the redirect URL. When it comes to getParametersForTokenRequest the client ID is not specified in the form.
Question: who is right here? How do I tell Spring that MS wants the client id in the token request after an authorization code has been obtained?
Just to clarify, you're not actually authenticating with or against Microsoft Graph. You're actually authenticating against Azure Active Directory. The Microsoft Graph API accepts the bearer token you'll end up with but it doesn't issue the access token itself.
It isn't clear which endpoint you're using for the Authorization Code flow, AAD has two of them: v1 and v2. The primary difference being that v2 uses a central registration and can authenticate both work/school and personal accounts.
Regardless of the endpoint, you do need to supply the clientid in the request body when you're requesting an access token. There are actually several values you need to provide in the body. Also note that these need to be provided as application/x-www-form-urlencoded.
For the v1 endpoint you provide (line breaks for readability only):
grant_type=authorization_code
&client_id={client-id}
&code={authoization-code}
&redirect_uri={redirect-uri}
&client_secret={client-secret}
&resource={resource-uri}
The v2 endpoint is almost identical but uses scope instead of resource:
grant_type=authorization_code
&client_id={client-id}
&code={authoization-code}
&redirect_uri={redirect-uri}
&client_secret={client-secret}
&scope={scopes}
OP's edit
Now, back to Spring Security. Spring by default uses an HTTP basic authentication scheme against Azure AD. In that scheme, the client ID and secret are encoded into the HTTP Authorization header, then the form only contains the authorization code and state parameter, so here is why I (the OP, ndr) was puzzled about why AAD refused the authorization.
In order to pass client ID and secret into the form, we can tell Spring Security to use a different supported authentication scheme. The form authentication scheme will push the client ID and secret into the form.
The below code works and retrieves the access token.
<oauth2:resource
id="msAdAuthenticationSource"
client-id="${oauth.appId}"
client-secret="${oauth.appSecret}"
type="authorization_code"
authentication-scheme="form"
client-authentication-scheme="form"
use-current-uri="true"
user-authorization-uri="${oauth.authorizationUri}"
access-token-uri="${oauth.accessTokenUri}"
scope="${oauth.scopes}"
pre-established-redirect-uri="${oauth.redirectUri}" />
Please note the two
authentication-scheme="form"
client-authentication-scheme="form"
Problem solved, a lot more to come!