It is a bit confusing to set up a resource server that is different from the auth server.
I'm not sure which configs should go in the resource server server and which in the auth server.
The resource server only needs the token validation part (JWT bearer middleware, OAuth2 validation or OAuth2 introspection middleware).
@shaunluttin is working on a sample that uses separate resource servers (https://github.com/openiddict/openiddict-samples/pull/8)
Looks good. I'll follow it.
However, it doesn't seem to be far enough along for me to use. Do you mind having a look at my project and letting me know where I'm going wrong with this please:
Auth server: https://github.com/jayrulez/Etherkeep/tree/master/src/Etherkeep.Accounts
Resource server: https://github.com/jayrulez/Etherkeep/tree/master/src/Etherkeep.Server
Do you mind having a look at my project and letting me know where I'm going wrong with this please:
I see at least one issue: the introspection endpoint is not enabled.
I enabled the introspection endpoint:
https://github.com/jayrulez/Etherkeep/commit/2e030d7fadb724cc37565fdb7f894d8f12e2ce60
The auth server seems to work fine when i interact directly with the auth server (I can get a token, refresh token, introspect etc...).
However, when I make a request to the resource server through postman, I am getting a 404 for all protected endpoints. I am expecting to get a 401 here.
e.g: GET http://localhost:5001/api/users
Is there anything interesting in the logs?
Yes:
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:5001/api/users
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:5001/api/users
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: Bearer was not authenticated. Failure message: Authentication failed because the access token was not valid for this resource server.
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: Bearer was not authenticated. Failure message: Authentication failed because the access token was not valid for this resource server.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: AuthenticationScheme: Bearer was challenged.
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 61.6816ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 61.6816ms 404
Authentication failed because the access token was not valid for this resource server.
This error is caused by an invalid audience.
It seems I don't get how this actually works.
So my auth server is http://localhost:5000
My resource server is http://localhost:5001
In the startup.cs file on my resource server, I have:
app.UseOAuthIntrospection(options =>
{
options.Authority = "http://localhost:5000/";
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.Audiences.Add("http://localhost:5001/");
options.ClientId = "resource_server";
options.ClientSecret = Crypto.HashPassword("secret_secret_secret");
});
I used postman to request a token using grant type = password with the client id and secret of my resource server.
I then tried to access a protected endpoint on the resource servr with the token I got using postman:
GET http://localhost:5001/api/users
Is it because I used postman to get the token why it is being rejected?
Also since the token is rejected, I am getting 404 and not 401. If I am unauthorized, should I not be getting a 401 instead?
I used postman to request a token using grant type = password with the client id and secret of my resource server.
To be able to configure an audience in the introspection options, your access tokens must contain this specific audience. And for that, you must use the resource token request parameter to set it.
Also since the token is rejected, I am getting 404 and not 401. If I am unauthorized, should I not be getting a 401 instead?
The 401 response is likely caught by app.UseIdentity() (why do you need it BTW?)
I actually do get a 401.
I overlooked adding the error controller back to the resource server when i separated it from the auth server. I was getting a 404 because the error endpoint could not be found.
This commit fixes that: https://github.com/jayrulez/Etherkeep/commit/5c99f0538b519ba92b945e8c8aad9424a7e28872
Making progress but I'm still having a bit of trouble.
I interpret your answer above as adding say:
resource=resource_server to the token request right?
Also, why do all the messages seem to be logged twice?
Is that because of: app.UseStatusCodePagesWithReExecute("/error");?
Latest logs:
"The thread 0x74c has exited with code 0 (0x0).
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:5000/connect/token application/x-www-form-urlencoded 162
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:5000/connect/token application/x-www-form-urlencoded 162
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory:Information: Executed DbCommand (3ms) [Parameters=[@__identifier_0='?'], CommandType='Text', CommandTimeout='30']
SELECT "application"."Id", "application"."ClientId", "application"."ClientSecret", "application"."DisplayName", "application"."LogoutRedirectUri", "application"."RedirectUri", "application"."Type"
FROM "OpenIddictApplications" AS "application"
WHERE "application"."ClientId" = @__identifier_0
LIMIT 2
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory:Information: Executed DbCommand (3ms) [Parameters=[@__identifier_0='?'], CommandType='Text', CommandTimeout='30']
SELECT "application"."Id", "application"."ClientId", "application"."ClientSecret", "application"."DisplayName", "application"."LogoutRedirectUri", "application"."RedirectUri", "application"."Type"
FROM "OpenIddictApplications" AS "application"
WHERE "application"."ClientId" = @__identifier_0
LIMIT 2
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory:Information: Executed DbCommand (1ms) [Parameters=[@__normalizedUserName_0='?'], CommandType='Text', CommandTimeout='30']
SELECT "u"."Id", "u"."AccessFailedCount", "u"."ConcurrencyStamp", "u"."Email", "u"."EmailConfirmed", "u"."FirstName", "u"."LastName", "u"."LockoutEnabled", "u"."LockoutEnd", "u"."NormalizedEmail", "u"."NormalizedUserName", "u"."PasswordHash", "u"."PhoneNumber", "u"."PhoneNumberConfirmed", "u"."SecurityStamp", "u"."Status", "u"."TwoFactorEnabled", "u"."Type", "u"."UserName"
FROM "AspNetUsers" AS "u"
WHERE "u"."NormalizedUserName" = @__normalizedUserName_0
LIMIT 1
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory:Information: Executed DbCommand (1ms) [Parameters=[@__normalizedUserName_0='?'], CommandType='Text', CommandTimeout='30']
SELECT "u"."Id", "u"."AccessFailedCount", "u"."ConcurrencyStamp", "u"."Email", "u"."EmailConfirmed", "u"."FirstName", "u"."LastName", "u"."LockoutEnabled", "u"."LockoutEnd", "u"."NormalizedEmail", "u"."NormalizedUserName", "u"."PasswordHash", "u"."PhoneNumber", "u"."PhoneNumberConfirmed", "u"."SecurityStamp", "u"."Status", "u"."TwoFactorEnabled", "u"."Type", "u"."UserName"
FROM "AspNetUsers" AS "u"
WHERE "u"."NormalizedUserName" = @__normalizedUserName_0
LIMIT 1
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 64.8642ms 200 application/json;charset=UTF-8
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 64.8642ms 200 application/json;charset=UTF-8
The thread 0x14b0 has exited with code 0 (0x0).
The thread 0x351c has exited with code 0 (0x0).
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:5001/api/users
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:5001/api/users
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: Bearer was not authenticated. Failure message: Authentication failed because the access token was not valid for this resource server.
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: Bearer was not authenticated. Failure message: Authentication failed because the access token was not valid for this resource server.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: AuthenticationScheme: Bearer was challenged.
AspNet.Security.OAuth.Introspection.OAuthIntrospectionMiddleware:Information: AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executing action method Etherkeep.Server.Controllers.ErrorController.Error (Etherkeep.Server) with arguments () - ModelState is Valid
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executing action method Etherkeep.Server.Controllers.ErrorController.Error (Etherkeep.Server) with arguments () - ModelState is Valid
Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor:Information: Executing ViewResult, running view at path /Views/Shared/Error.cshtml.
Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor:Information: Executing ViewResult, running view at path /Views/Shared/Error.cshtml.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action Etherkeep.Server.Controllers.ErrorController.Error (Etherkeep.Server) in 10.3154ms
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action Etherkeep.Server.Controllers.ErrorController.Error (Etherkeep.Server) in 10.3154ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 90.2548ms 401 text/html; charset=utf-8
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 90.2548ms 401 text/html; charset=utf-8
"
I finally understand this now. I need to add the resource server as an audience in the introspection config. I need to pass the resource server to the token request endpoint.
For my use case, I do not need to limit the audience of a token so I don't need to do any of this.
Thank you for your assistance @PinpointTownes
Dear,
Come some one help me? I have a similar issue; my auth server and resource server are in different projects.
I configured my resource server startup in this way:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OAuthIntrospectionDefaults.AuthenticationScheme;
})
.AddOAuthIntrospection(options =>
{
options.Authority = new Uri(OAuthConfig.Authority);
options.Audiences.Add(OAuthConfig.Audience);
options.ClientId = OAuthConfig.ClientId;
options.ClientSecret = OAuthConfig.ClientSecret;
if (environment.IsDevelopment())
options.RequireHttpsMetadata = false;
});
My config look like this:
"OAuth": {
"Authority": "http://192.168.1.80:6010/",
"ClientId": "myClientId",
"ClientSecret": "blablabla",
"Audience": "resource_server"
}
I already share data protection key with redis.
But when i call my resource server with postman i receive this message from Auth Server:
warn: OpenIddict.OpenIddictProvider[0]
The client application 'myClientId' is not allowed to introspect the access token '89ad8f93-867e-4ce6-be95-e392394d09e4' because it's not listed as a valid audience.
the step to repro are:
@michaelsogos the client_id of your resource server doesn't match the resource value you store in the authentication ticket. Rename myClientId to resource_server and it should work.
It works, thanks. (i did the opposite but it is the same) :)
_I'd like to leave here an explanation for next coming._
An resource must be equals to the clientId to be valid when using the introspection endpoint.
The case is exactly when you want to implement Auth Server separated by Resource API (example when you implement microservices architecture).
The introspection endpoint enter in game when a user (grant_type:password + username + password) want to access to a Resource API, and if your api is under authorization check with [Authorize] (don't forget to use this [Authorize(AuthenticationSchemes = OAuthIntrospectionDefaults.AuthenticationScheme)]), under the hood it check the Authority URL (configured like in my example above) to know which is the introspection endpoint.
This is the way how Resource API verify the bearer token received on http header with Authorization:Bearer [TOKEN].
Once the Auth Server receive the call on /connect/introspect endpoint (keep in mind it is implemented by openiddict so do not create your custom controller\action like /connect/token), openiddict verify if the token supplied is for a user (do not confuse with client) that has access to that Resource API.
So is very very important that the /connect/token create a ticket for the user that contains the right resource name (if you follow example around you should see something like ticket.SetResources("the_client_id");), in this way every time a user authenticate thru /connect/token the released token contains a list of clientId that user can access, later on the Resource API associated to a specific clientId (like in my example above) call /connect/introspect on Auth Server that internally will check if there is matching between user resources and clientId (that are the same thing) :)
IMPORTANT NOTE
You have to share the same DataProtection Key between Auth Server and Resource APIs, this link work well for me (i'm creating a Microservice architecture on docker so my choice is Redis storage, but many other are available)
That's all.
The best example that reply exactly the same situation described here is implicit flow, even if i didn't used that flow in my implementation because my security is entirely managed on server side, so is possible to implement it also with password credential flow and client credential flow.
Thanks for the detailed feedback, @michaelsogos :+1:
Can i suggest you to add the explanation on wiky or at least on readme.md?
Most helpful comment
It works, thanks. (i did the opposite but it is the same) :)
_I'd like to leave here an explanation for next coming._
An
resourcemust be equals to theclientIdto be valid when using the introspection endpoint.The case is exactly when you want to implement Auth Server separated by Resource API (example when you implement microservices architecture).
The introspection endpoint enter in game when a user (grant_type:password + username + password) want to access to a Resource API, and if your api is under authorization check with
[Authorize](don't forget to use this[Authorize(AuthenticationSchemes = OAuthIntrospectionDefaults.AuthenticationScheme)]), under the hood it check the Authority URL (configured like in my example above) to know which is the introspection endpoint.This is the way how Resource API verify the bearer token received on http header with Authorization:Bearer [TOKEN].
Once the Auth Server receive the call on
/connect/introspectendpoint (keep in mind it is implemented by openiddict so do not create your custom controller\action like/connect/token), openiddict verify if the token supplied is for a user (do not confuse with client) that has access to that Resource API.So is very very important that the
/connect/tokencreate a ticket for the user that contains the right resource name (if you follow example around you should see something liketicket.SetResources("the_client_id");), in this way every time a user authenticate thru/connect/tokenthe released token contains a list ofclientIdthat user can access, later on the Resource API associated to a specificclientId(like in my example above) call/connect/introspecton Auth Server that internally will check if there is matching between userresourcesandclientId(that are the same thing) :)IMPORTANT NOTE
You have to share the same DataProtection Key between Auth Server and Resource APIs, this link work well for me (i'm creating a Microservice architecture on docker so my choice is Redis storage, but many other are available)
That's all.
The best example that reply exactly the same situation described here is implicit flow, even if i didn't used that flow in my implementation because my security is entirely managed on server side, so is possible to implement it also with password credential flow and client credential flow.