Http status 401 spring cloud gateway token relay - spring-security

I have a spring cloud gateway service as Oauth2 client and Token Relay, which is configured with Oauth2 resource server. This setup works fine using Okta as authorization server but changing to Google, results in an HTTP 401 when the gateway tries to request the secured service. Below my configurations:
Gateway aplication.yml
server:
port: 80
spring:
application:
name: gateway-service
security:
oauth2:
client:
registration:
okta:
provider: okta
client-id: ${OKTA_OAUTH2_CLIENT_ID}
client-secret: ${OKTA_OAUTH2_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri-template: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: openid,profile,email
google:
provider: google
client-id: ${GOOGLE_OAUTH2_CLIENT_ID}
client-secret: ${GOOGLE_OAUTH2_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri-template: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: openid,profile,email
provider:
okta:
issuer-uri: <my_okta_uri>
google:
issuer-uri: https://accounts.google.com
cloud:
gateway:
routes:
- id: user
uri: lb://USER-SERVICE
predicates:
- Path=/user/**
filters:
- TokenRelay=
- RewritePath=/user(?<params>/?.*), $\{params}
- id: category
uri: lb://CATEGORY-SERVICE
predicates:
- Path=/category/**
filters:
- RewritePath=/category(?<params>/?.*), $\{params}
- id: income
uri: lb://INCOME-SERVICE
predicates:
- Path=/income/**
filters:
- RewritePath=/income(?<params>/?.*), $\{params}
- id: expense
uri: lb://EXPENSE-SERVICE
predicates:
- Path=/expense/**
filters:
- RewritePath=/expense(?<params>/?.*), $\{params}
- id: budget
uri: lb://BUDGET-SERVICE
predicates:
- Path=/budget/**
filters:
- RewritePath=/budget(?<params>/?.*), $\{params}
management:
endpoints:
web:
exposure:
include: "*"
logging:
level:
root: INFO
org:
springframework:
cloud:
gateway:
filter: TRACE
Protected service application.yml
server:
port: 8081
spring:
cloud:
loadbalancer:
ribbon:
enabled: false
application:
name: user-service
data:
mongodb:
uri: ${MONGO_URI}
security:
oauth2:
resourceserver:
jwt:
# issuer-uri: <my_okta_uri>
issuer-uri: https://accounts.google.com
management:
endpoints:
web:
exposure:
include: "*"
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.web.HttpLogging: DEBUG
org.springframework.security: DEBUG
org.springframework.security.oauth2: DEBUG
org.springframework.cloud.gateway: DEBUG

I had a similar problem with Auth0. It looked like the TokenRelay filter was not passing the JWT as the Bearer token. It was providing a different access token from the OAuth2AuthorizedClient. There may be a way to provide a different OAuth2AuthorizedClient or configure this differently, but I couldn't figure it out.
However, the exchange provided to the filter was already authenticated, so it had the JWT token I wanted to send to my backend services. I think this was because my backend service endpoints were configured as authenticated:
#Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.pathMatchers("/service/**").authenticated()
.pathMatchers("/**").permitAll();
http.oauth2Login();
return http.build();
}
So I wrote a custom OidcTokenRelayGatewayFilterFactory:
public class OidcTokenRelayGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
public OidcTokenRelayGatewayFilterFactory() {
super(Object.class);
}
#Override
public String name() {
return "OidcTokenRelay";
}
#Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> exchange.getPrincipal()
.filter(principal -> principal instanceof OAuth2AuthenticationToken)
.cast(OAuth2AuthenticationToken.class)
.map(OAuth2AuthenticationToken::getPrincipal)
.filter(principal -> principal instanceof OidcUser)
.cast(OidcUser.class)
.map(token -> withOidcIdToken(exchange, token.getIdToken()))
.defaultIfEmpty(exchange).flatMap(chain::filter);
}
private ServerWebExchange withOidcIdToken(ServerWebExchange exchange, OidcIdToken accessToken) {
return exchange.mutate().request(r -> r.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue())))
.build();
}
}
Then I registered a bean for the FilterFactory like how the original TokenRelayGatewayFilterFactory worked:
#Configuration(proxyBeanMethods = false)
#ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
#ConditionalOnClass({ OAuth2AuthorizedClient.class, SecurityWebFilterChain.class, SecurityProperties.class })
#ConditionalOnEnabledFilter(OidcTokenRelayGatewayFilterFactory.class)
public class OidcTokenRelayConfiguration {
#Bean
public OidcTokenRelayGatewayFilterFactory myTokenRelayGatewayFilterFactory() {
return new OidcTokenRelayGatewayFilterFactory();
}
}
Finally, I updated my Spring configuration file to use my custom filter.
spring:
application:
name: cluster-api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
filters:
- OidcTokenRelay=
- StripPrefix=2
predicates:
- Path="/service/{serviceId}/**"
With these changes, I was able to route requests to my backend services without receiving a 401 response.

Related

Nuxt auth-next with keycloak CORS problem

Versions:
keycloak 12.0.2
nuxt: 2.14.6
nuxt/auth-next: 5.0.0-1622918202.e815752
Configs:
nuxt.config.js
auth: {
strategies: {
keycloak: {
scheme: '~/plugins/keycloak.js',
endpoints: {
authorization:'https://keycloak.bgzchina.com/auth/realms/bgzchina/protocol/openid-connect/auth',
token:'https://keycloak.bgzchina.com/auth/realms/bgzchina/protocol/openid-connect/token',
userInfo: "https://keycloak.bgzchina.com/auth/realms/bgzchina/protocol/openid-connect/token",
logout:'https://keycloak.bgzchina.com/auth/realms/bgzchina/protocol/openid-connect/logout',
},
responseType: 'id_token token',
clientId: 'centuari-portal-fe',
scope: ['openid'],
}
},
redirect: {
login: '/login',
logout: '/logout',
callback: '/callback',
home: '/',
}
},
router: {
middleware: ['auth']
},
due to a issue with current version nuxt/auth-next, I created a custom scheme by extending oauth2
/plugin/keycloak.js
import { Oauth2Scheme } from '~auth/runtime'
function encodeQuery(queryObject) {
return Object.entries(queryObject)
.filter(([_key, value]) => typeof value !== 'undefined')
.map(([key, value]) => encodeURIComponent(key) + (value != null ? '=' + encodeURIComponent(value) : ''))
.join('&')
}
export default class KeycloakScheme extends Oauth2Scheme {
logout() {
if (this.options.endpoints.logout) {
const opts = {
client_id: this.options.clientId,
post_logout_redirect_uri: this._logoutRedirectURI
}
const url = this.options.endpoints.logout + '?' + encodeQuery(opts)
window.location.replace(url)
}
return this.$auth.reset()
}
}
but when doing login, browser will block the token request due to CORS. keycloak server response for the preflight specify allowed method is POST, OPTIONS, but auth-next use GET to fetch token.
Is there any work around ?
You need to add/register the url into keycloak admin dashboard.
Go to keycloak admin dashboard
Menu Clients => select the client
On Settings tab, scroll down the page and find Web Origins. Add your frontend url (nuxt url) on it. Don't forget to add into Valid Redirect URIs too.

Next-Auth Okta Authorization code with PKCE

I am trying to integrate a NextJS application with Okta, using the Authorization code flow with PKCE. The flow is not complete because the token request is not being performed.
This is the configuration for the provider:
import NextAuth from 'next-auth';
const oktaBaseUrl = 'https://my-okta-domain.com/oauth2/[auth-server-id]';
const clientId = '[My Client Id]';
const authorizationUrl =
oktaBaseUrl +
'/v1/authorize?response_type=code&response_mode=query&state=false';
const accessTokenUrl = oktaBaseUrl + '/v1/token';
const profileUrl = oktaBaseUrl + '/v1/userinfo';
export default NextAuth({
providers: [
{
id: 'okta',
name: 'Okta',
type: 'oauth',
version: '2.0',
protection: 'pkce',
clientId,
clientSecret: '',
accessTokenUrl,
authorizationUrl,
profileUrl,
scope: 'services',
params: {
grant_type: 'authorization_code',
},
profile(profile) {
return {
id: profile.id as string,
name: profile.name,
email: profile.email
};
}
}
],
});
Thefirst stage seems to be perfromed correctly, but when okta returns the code, I only see a message in my application showing an 403 code. It seems it is trying to get the code without perform the request to the token endpoint
Message in console:
[next-auth][error][oauth_get_access_token_error]
https://next-auth.js.org/errors#oauth_get_access_token_error { statusCode: 403, data: '' } okta y64EzO0u9ZbwqFdjJWqapXggDmC1bWx2DGQaITCpta4
[next-auth][error][oauth_callback_error]
https://next-auth.js.org/errors#oauth_callback_error { statusCode: 403, data: '' }
Is there a configuration I am missing?

Nuxt.js exchanging authorization code for access token Oauth2

I'm trying to exchange the authorization code I got in the first step of the documentation for access token. Where I'm stuck is how to send a request for the token that contains the code I've just got with the first request.
This is my code:
auth: {
redirect: {
login: '/',
callback: '/auth'
},
strategies: {
wrike: {
scheme: 'oauth2',
endpoints: {
authorization: 'https://login.wrike.com/oauth2/authorize/v4',
token: 'https://login.wrike.com/oauth2/token',
logout: '/'
},
token: {
property: 'access_token',
type: 'Bearer',
maxAge: 1800
},
responseType: 'code',
grantType: 'authorization_code',
accessType: 'offline',
clientId: XXXX,
client_secret: YYYY
}
}
}
I can't figure it out how I should set up the redirect URI, in the client or in the server side? How should I do the second request? (This below)
POST https://login.wrike.com/oauth2/token
//Parameters:
client_id=<client_id>
client_secret=<client_secret>
grant_type=authorization_code
code=<authorization_code>
I think Edward is right. It doesn't seem to work. You can either do the custom schema which is what I am going to do, or you can do what I currently have which is something like this (of course ignore all the console.log and stuff like that):
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
const state = urlParams.get('state')
console.log('state', state)
console.log('stateStorage', window.localStorage.getItem(state))
if ((code && state) && state === window.localStorage.getItem('state')) {
this.$axios.post('http://publisher-local.co.uk:8080/oauth/token', {
grant_type: 'authorization_code',
client_id: 5,
redirect_uri: 'http://localhost:3000/auth',
code_verifier: window.localStorage.getItem('verifier'),
code
}).then(response => {
this.$auth.setUserToken(response.data.access_token)
this.$auth.fetchUser()
})
}
So basically after you are redirected back to your client after logging in on the server page just look for the details in the URL and make the request yourself.

Hapi Bell Twitter Auth with email

I'm struggling to get the email address of twitter users when they login
I get the following error from Joi "Error: Uncaught error: Invalid options value: must be true, falsy or an object"
server.auth.strategy('twitter', 'bell', {
provider: 'twitter',
scope: ['public_profile', 'email'],
config: {
extendedProfile: true,
getParams: 'include_email',
getMethod: 'account/verify'
},
password: config.longpass, //Use something more secure in production
clientId: config.twitter_key,
clientSecret: config.twitter_secret,
isSecure: config.useHttps //Should be set to true (which is the default) in production
});
This code works for me.
server.auth.strategy('twitter', 'bell', {
provider: 'twitter',
config: {
getMethod: 'account/verify_credentials',
getParams: {include_email:'true' },//doesn't work without quotes!
},
password: 'secret_cookie_encryption_password', //Use something more secure in production
clientId: secret.twitterId,
clientSecret: secret.twitterSecret,
isSecure: false //Should be set to true (which is the default) in production
});
Don't forget to allow necessary permission (Request email addresses from users) in your Twitter application settings.

Swagger, JWT, how to use token in calls after authentication

I'm new to swagger.
We have an API written already so I'm trying manually write the swagger.yaml
So far I have figured out how to do my /login route.. and get back a JWT in the response.
But I'm not sure what way to go next.
Is it possible to automatically plug the returned JWT into subsequent calls?
Or do I have to manually copy and paste the returned JWT?
If I have to manually do it.. then.. ehh.. how?
In the swagger editor an Authenticate button appears and I can click that and get an input box looking for the apikey...
But its not the same when viewing the swagger UI ... when I browse to localhost to see the swagger UI I don't get the authenticate button and don't have anywhere to paste the JWT text...
My swagger.yaml is as follows:
swagger: "2.0"
info:
version: 1.0.0
title: Identity Management Service
description: API to allow JWT authentication and authorisation
termsOfService: http://swagger.io/terms/
license:
name: MIT
url: http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
host: localhost:8000
basePath: /
schemes:
- http
- https
securityDefinitions:
Bearer:
type: apiKey
name: Authorization
in: header
consumes:
- application/json
produces:
- application/json
paths:
/login:
post:
summary: User Authentication returning a JWT.
description: Authenticate a user.
parameters:
- name: credentials
in: body
description: maximum number of results to return
required: false
schema:
$ref: '#/definitions/creds'
responses:
"200":
description: will send JWT
default:
description: unexpected error
schema:
$ref: '#/definitions/Error'
/getUsers:
get:
summary: Gets list of all users
description: Authenticate a user.
security:
- Bearer: []
responses:
"200":
description: will send JWT
default:
description: unexpected error
schema:
$ref: '#/definitions/Error'
definitions:
creds:
type: object
required:
- username
- password
properties:
username:
type: string
password:
type: string
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
Obviously I'd much prefer to have it so that the response token from the /login call be stored and used in the /getUsers ...
The response from a call to /login looks like this:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoidXNlciIsInVzZXJpZCI6InBqbWVhbHkiLCJlbWFpbCI6InBqbWVhbHlAZ21haWwuY29tIiwiZmlyc3RuYW1lIjoiUEoiLCJsYXN0bmFtZSI6Ik1lYWx5Iiwib3JnIjoib3JnMSIsInRlYW1zIjpbInRlYW0xIl0sImFjbCI6WyJlbXBsb3llZSIsInRlYW1MZWFkIl0sInRva2VuVHlwZSI6IndlYkFwcFRva2VuIiwidG9rZW5WZXJzaW9uIjoiMSIsImlhdCI6MTQ2NzkxMDkyNSwiZXhwIjoxNDY3OTk3MzI1fQ.e4Trk-0kDoid5Xr9BQ5ZP_HMBN2l8_G2pn7ac2tt4uE",
"user": {
"type": "user",
"userid": "joebloggs",
"email": "joe#bloggs.com",
"firstname": "Joe",
"lastname": "Bloggs",
"org": "org1",
"teams": [
"team1"
],
"acl": [
"employee",
"teamLead"
],
"tokenType": "webAppToken",
"tokenVersion": "1",
"iat": 1467910925,
"exp": 1467997325
}
}
You can try this, it include an Authorization Header where you can save the token and it will apply to all endpoints.
#Bean
public Docket newsApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.securitySchemes(Lists.newArrayList(apiKey()))
.securityContexts(Lists.newArrayList(securityContext()))
.apiInfo(generateApiInfo());
}
#Bean
SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.any())
.build();
}
List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope
= new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Lists.newArrayList(
new SecurityReference("JWT", authorizationScopes));
}
private ApiKey apiKey() {
return new ApiKey("JWT", "Authorization", "header");
}

Resources