Spring Cloud API Gateway Custom Filters with external API for Authorization - spring-webflux

I have a spring cloud gateway app with a custom filter to do Authorization in a route.
The route target is a blocking api also.
The Authorization filter fetches the permissions of the users from an external auth api using jwt token (blocking).
Then I am trying to check if the required permission is there , if not throw 401 .
But I am not getting the expected result as I am not able to figure how to handle the Mono from the permissions API.
The response is 200 even if the authorization is failing .
If the Authorization is allowed then I get the proper response from the downstream api in route.
Filter Code
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
//API to get permissions
Mono<PermissionResponse> permissionsOfUser = getPermissions(authorizationHeader);
return permissionsOfUser.flatMap(pu->{
//doing permission check
if(!isAuthorized(pu)){
return Mono.error(new AuthorizationException("Invalid"));
}else {
return chain.filter(exchange);
}
})
.doOnError(auth->this.onError(exchange,HttpStatus.UNAUTHORIZED))
.doOnNext(auth->chain.filter(exchange));
};
}
Authorization Check
private boolean isAuthorized(PermissionResponse permissionResponse) {
if(permissionResponse.getFunctions().stream().map(f->f.getFuncKey()).collect(Collectors.toList()).contains("EDIT_ACCESS")){
log.info("Has Permissions");
return true;
}else{
log.info("No Permissions");
return false;
}
}
API Call to get permissions
private Mono<PermissionResponse> getPermissions(String authorizationHeader) {
return webClientBuilder.build()
.get()
.uri(authPermissionsUrl)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, authorizationHeader)
.retrieve()
.bodyToMono(PermissionResponse.class)
.subscribeOn(Schedulers.immediate())
.publishOn(Schedulers.immediate());
}
Error Response
private Mono<Void> onError(ServerWebExchange exchange, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
Routing
cloud:
gateway:
default-filters:
- name: CustomAuthorizationFilter
routes:
- id: app-id
uri: http://myapi.com
predicates:
- Path=/getMyApiDetails/**
My thought is that I am not properly consuming the permission API Mono and not able to return the error from the filter.
I am new to webflux and not sure how to properly chain this.
log
2022-08-26 10:22:03.314 ERROR 16068 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [a7062490-6] 500 Server Error for HTTP GET "/getMyApiDetails/getDetails"
com.gateway.config.AuthorizationException: Invalid
at com.gateway.filters.CustomAuthorizationFilter.lambda$apply$0(CustomAuthorizationFilter.java:60) ~[classes/:na]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
Original Stack Trace:
at
com.gateway.filters.CustomAuthorizationFilter.lambda$apply$0(CustomAuthorizationFilter.java:60) ~[classes/:na]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:125) ~[reactor-core-3.4.19.jar:3.4.19]
at reactor.core.publisher.MonoPublishOn$PublishOnSubscriber.run(MonoPublishOn.java:181) ~[reactor-core-3.4.19.jar:3.4.19]

Related

How to redirect from GraphQL middleware resolver on authentication fail?

Introduction:
I am using GraphQL Mesh as a gateway between my app and an API. I use Apollo Client as the GraphQL client. When a user wants to visit the first screen after hitting the log-in button, I do a query to load data from a CMS. This query has to go through the gateway. In the gateway I do an auth check to see if the user has a valid JTW access token, if not, I want to redirect back to the sign-in page. If the user has a token, he is let through.
The gateway is-auth.ts resolver:
const header = context.headers.authorization;
if (typeof header === "undefined") {
return new Error("Unauthorized: no access token found.");
} else {
const token = header.split(" ")[1];
if (token) {
try {
const user = jwt.verify(token, process.env.JWT_SECRET as string);
} catch (error) {
return new Error("Unauthorized: " + error);
}
} else {
return new Error("Unauthorized: no access token found.");
}
}
return next(root, args, context, info);
},
Problem: Right now, I am returning Errors in the authentication resolver of the gateway, hoping that I could pick them up in the error object that is sent to Apollo Client and then redirect off of that. Unfortunately, I don't get that option, since the Errors are thrown immediately, resulting in an error screen for the user (not what I want). I was hoping this would work in order to redirect to the sign-in from the client-side, but it does not work:
const { data, error } = await apolloClient(accessToken).query({
query: gql`
query {
...where my query is.
}
`,
});
if (error) {
return {
redirect: {
permanent: false,
destination: `/sign-in`,
},
};
}
Does anyone perhaps have a solution to this problem?
This is the GraphQL Mesh documentation on the auth resolver, for anyone that wants to see it: https://www.graphql-mesh.com/docs/transforms/resolvers-composition. Unfortunately, it doesn't say anything about redirects.
Kind regards.

OAuth with KeyCloak in Ktor : Is it supposed to work like this?

I tried to set up a working Oauth2 authorization via Keycloak in a Ktor web server. The expected flow would be sending a request from the web server to keycloak and logging in on the given UI, then Keycloak sends back a code that can be used to receive a token. Like here
First I did it based on the examples in Ktor's documentation. Oauth It worked fine until it got to the point where I had to receive the token, then it just gave me HTTP status 401. Even though the curl command works properly. Then I tried an example project I found on GitHub , I managed to make it work by building my own HTTP request and sending it to the Keycloak server to receive the token, but is it supposed to work like this?
I have multiple questions regarding this.
Is this function supposed to handle both authorization and getting the token?
authenticate(keycloakOAuth) {
get("/oauth") {
val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
call.respondText("Access Token = ${principal?.accessToken}")
}
}
I think my configuration is correct, since I can receive the authorization, just not the token.
const val KEYCLOAK_ADDRESS = "**"
val keycloakProvider = OAuthServerSettings.OAuth2ServerSettings(
name = "keycloak",
authorizeUrl = "$KEYCLOAK_ADDRESS/auth/realms/production/protocol/openid-connect/auth",
accessTokenUrl = "$KEYCLOAK_ADDRESS/auth/realms/production/protocol/openid-connect/token",
clientId = "**",
clientSecret = "**",
accessTokenRequiresBasicAuth = false,
requestMethod = HttpMethod.Post, // must POST to token endpoint
defaultScopes = listOf("roles")
)
const val keycloakOAuth = "keycloakOAuth"
install(Authentication) {
oauth(keycloakOAuth) {
client = HttpClient(Apache)
providerLookup = { keycloakProvider }
urlProvider = { "http://localhost:8080/token" }
}
}
There is this /token route I made with a built HTTP request, this one manages to get the token, but it feels like a hack.
get("/token"){
var grantType = "authorization_code"
val code = call.request.queryParameters["code"]
val requestBody = "grant_type=${grantType}&" +
"client_id=${keycloakProvider.clientId}&" +
"client_secret=${keycloakProvider.clientSecret}&" +
"code=${code.toString()}&" +
"redirect_uri=http://localhost:8080/token"
val tokenResponse = httpClient.post<HttpResponse>(keycloakProvider.accessTokenUrl) {
headers {
append("Content-Type","application/x-www-form-urlencoded")
}
body = requestBody
}
call.respondText("Access Token = ${tokenResponse.readText()}")
}
TL;DR: I can log in via Keycloak fine, but trying to get an access_token gives me 401. Is the authenticate function in ktor supposed to handle that too?
The answer to your first question: it will be used for both if that route corresponds to the redirect URI returned in urlProvider lambda.
The overall process is the following:
A user opens http://localhost:7777/login (any route under authenticate) in a browser
Ktor makes a redirect to authorizeUrl passing necessary parameters
The User logs in through Keycloak UI
Keycloak redirects the user to the redirect URI provided by urlProvider lambda passing parameters required for acquiring an access token
Ktor makes a request to the token URL and executes the routing handler that corresponds to the redirect URI (http://localhost:7777/callback in the example).
In the handler you have access to the OAuthAccessTokenResponse object that has properties for an access token, refresh token and any other parameters returned from Keycloak.
Here is the code for the working example:
val provider = OAuthServerSettings.OAuth2ServerSettings(
name = "keycloak",
authorizeUrl = "http://localhost:8080/auth/realms/master/protocol/openid-connect/auth",
accessTokenUrl = "http://localhost:8080/auth/realms/$realm/protocol/openid-connect/token",
clientId = clientId,
clientSecret = clientSecret,
requestMethod = HttpMethod.Post // The GET HTTP method is not supported for this provider
)
fun main() {
embeddedServer(Netty, port = 7777) {
install(Authentication) {
oauth("keycloak_oauth") {
client = HttpClient(Apache)
providerLookup = { provider }
// The URL should match "Valid Redirect URIs" pattern in Keycloak client settings
urlProvider = { "http://localhost:7777/callback" }
}
}
routing {
authenticate("keycloak_oauth") {
get("login") {
// The user will be redirected to authorizeUrl first
}
route("/callback") {
// This handler will be executed after making a request to a provider's token URL.
handle {
val principal = call.authentication.principal<OAuthAccessTokenResponse>()
if (principal != null) {
val response = principal as OAuthAccessTokenResponse.OAuth2
call.respondText { "Access token: ${response.accessToken}" }
} else {
call.respondText { "NO principal" }
}
}
}
}
}
}.start(wait = false)
}

Log middle-ware gets 200 response code when it is actually a 500

I am working on a ASP.Net Core Web API project and I want to log all the requests and 500 server errors.
I used custom middleware to log requests, and it is defined in the startup.cs as:
app.UseMiddleware<logMiddleware>();
I also defined an Exception handler to capture server errors in the startup.cs:
app.UseExceptionHandler(builder => {
builder.Run(async context => {
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null) {
logService logger = new logService(conf);
await logger.logError(error, context);
}
});
});
I keep the request information in error logs, so I don't need to save the request log when the response code is 500, so I added a check into request log function to filter errors:
public async Task logRequestAsync(HttpContext context) {
if (context.Response.StatusCode != 500) {
//do things
}
}
The problem is the Response.StatusCode returns as 200 instead of 500. Probably it runs before the API call's function is completed and the server error happens later in the runtime.
Is there a way to move the "request log" process to the point where the response is created instead of the beginning of the API request?

How to customize the authorization error produced by OpenIddict?

I'm using OpenIddict for auth in a .NET Core 2 API. Client side I'm relying on any API errors to follow a custom scheme. However, when e.g. a refresh token has been outdated, I can't seem to find out how to customize the error sent back.
The /token endpoint is never reached, so the error is not under "my control".
The result of the request is a status code 400, with the following JSON:
{"error":"invalid_grant","error_description":"The specified refresh token is no longer valid."}
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
How can I properly customize the error or intercept to change it? Thanks!
You can use OpenIddict's event model to customize the token response payloads before they are written to the response stream. Here's an example:
MyApplyTokenResponseHandler.cs
public class MyApplyTokenResponseHandler : IOpenIddictServerEventHandler<ApplyTokenResponseContext>
{
public ValueTask HandleAsync(ApplyTokenResponseContext context)
{
var response = context.Response;
if (string.Equals(response.Error, OpenIddictConstants.Errors.InvalidGrant, StringComparison.Ordinal) &&
!string.IsNullOrEmpty(response.ErrorDescription))
{
response.ErrorDescription = "Your customized error";
}
return default;
}
}
Startup.cs
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>
{
// ...
options.AddEventHandler<ApplyTokenResponseContext>(builder =>
builder.UseSingletonHandler<MyApplyTokenResponseHandler>());
})
.AddValidation();
The /token endpoint is never reached, so the error is not under "my control".
In fact ,the /token is reached, and the parameter of grant_type equals refresh_token. But the rejection logic when refresh token expired is not processed by us. It is some kind of "hardcoded" in source code :
if (token == null)
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
if (!await TryRedeemTokenAsync(token))
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
}
The context.Reject here comes from the assembly AspNet.Security.OpenIdConnect.Server.
For more details, see source code on GitHub .
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
I've tried and I'm pretty sure we can use a custom middleware to catch all status codes. The key point is to detect the status code after the next() invocation:
app.Use(async(context , next )=>{
// passby all other end points
if(! context.Request.Path.StartsWithSegments("/connect/token")){
await next();
return;
}
// since we might want to detect the Response.Body, I add some stream here .
// if you only want to detect the status code , there's no need to use these streams
Stream originalStream = context.Response.Body;
var hijackedStream = new MemoryStream();
context.Response.Body = hijackedStream;
hijackedStream.Seek(0,SeekOrigin.Begin);
await next();
// if status code not 400 , pass by
if(context.Response.StatusCode != 400){
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
return;
}
// read and custom the stream
hijackedStream.Seek(0,SeekOrigin.Begin);
using (StreamReader sr = new StreamReader(hijackedStream))
{
var raw= sr.ReadToEnd();
if(raw.Contains("The specified refresh token is no longer valid.")){
// custom your own response
context.Response.StatusCode = 401;
// ...
//context.Response.Body = ... /
}else{
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
}
}
});
// helper to make the copy easy
private async Task CopyStreamToResponseBody(HttpContext context,Stream newStream, Stream originalStream){
newStream.Seek(0,SeekOrigin.Begin);
await newStream.CopyToAsync(originalStream);
context.Response.ContentLength =originalStream.Length;
context.Response.Body = originalStream;
}

Bearer token 401 authorization error from Angular5 APP to .Net 4.7 back-end API

Working with Angular 5 on in our app, we are authenticating through the Azure Active Directory(AAD), getting a bearer token to access the backend API. We are not having any success in the athentication call to the backend api, getting a 401 unauthrized. Both App and Api is hosted in Azure. Any help would be appreciated in troubleshooting this issue:
The App service Registration on AAD:
Angular App (using the adal-angular5 1.0.36 library):
Application ID: aaaaa-aaaaa-aaaaa-aaaaa-aaaaa
Object ID: bbbbbb-bbbbb-bbbb-bbbbb-bbbbbb
App ID URI: https://frontendapp.azurewebsites.net
Home Page URL: https://homepage.frontendapp.com
Backend API (.Net 4.7):
Application ID: cccccc-cccccc-cccccc-cccccc-cccccc
Object ID: dddddd-ddddd-ddddd-dddddd-dddddd
App ID URI: https://backendapi.azurewebsites.net
Home Page URL: https://homepage.backendapi.com
App side
config for adal-angular5 on the Angular App:
config: adal.Config = {
tenant: 'common',
clientId: 'aaaaa-aaaaa-aaaaa-aaaaa-aaaaa',
postLogoutRedirectUri: window.location.origin,
endpoints: {
ApiUri: "https://homepage.backendapi.com",
}
};
Call to get the bearer token:
this.adal5Service.acquireToken("https://backendapi.azurewebsites.net")
call to the api
we verified the options contains the header which has the bearer token:
this.adal5HttpService.get('/api/hello', options);
On the API side
we are using the Microsoft Owin library (Microsoft.Owin 4.0.0 nuget package),
API startup:
public partial class Startup
{
/// <summary>
/// Initializes OWIN authentication.
/// </summary>
/// <param name="app">Web API AppBuilder</param>
public void Configuration(IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException("app");
}
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = "common",
Audience = "https://backendapi.azurewebsites.net"
});
}
}
API side controler
[HttpGet]
[Authorize]
[Route("hello")]
public string GetHello()
{
try
{
var result = "we hit it yay!!!!!!!!!!!!!!!";
return result;
}
catch (Exception ex)
{
var msg = "not hit saddd.....";
var error = new Exception(msg, ex);
throw error;
}
}
With the [Authorize] attribute we are getting a 401 unauthorized, with no additional error message.
Without the [Authorize] attribute we are hitting the Controller method just fine and getting the return results.
I was able to confirm the bearer token that's sent from the App is indeed the bearer token we get in the header in our Api. I can't figure out why we are getting the unauthorized.
Is the config setting in correct on the APP or the API side?
Is there additional configuration needed in Azure?
Any help would be appreciate it!