New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sample where resource server is separate from auth server #180

Closed
jayrulez opened this Issue Jul 30, 2016 · 17 comments

Comments

Projects
None yet
3 participants
@jayrulez
Copy link

jayrulez commented Jul 30, 2016

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.

@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Jul 30, 2016

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 (openiddict/openiddict-samples#8)

@jayrulez

This comment has been minimized.

Copy link

jayrulez commented Jul 30, 2016

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

@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Jul 30, 2016

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.

@jayrulez

This comment has been minimized.

Copy link

jayrulez commented Jul 30, 2016

I enabled the introspection endpoint:

jayrulez/Etherkeep@2e030d7

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

@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Jul 30, 2016

Is there anything interesting in the logs?

@jayrulez

This comment has been minimized.

Copy link

jayrulez commented Jul 31, 2016

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

@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Jul 31, 2016

Authentication failed because the access token was not valid for this resource server.

This error is caused by an invalid audience.

@jayrulez

This comment has been minimized.

Copy link

jayrulez commented Jul 31, 2016

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?

@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Jul 31, 2016

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?)

@jayrulez

This comment has been minimized.

Copy link

jayrulez commented Jul 31, 2016

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: jayrulez/Etherkeep@5c99f05

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
"

@jayrulez

This comment has been minimized.

Copy link

jayrulez commented Jul 31, 2016

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

@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Aug 14, 2016

@michaelsogos

This comment has been minimized.

Copy link

michaelsogos commented Aug 22, 2017

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:

  1. Get token with PASSWORD CREDENTIAL FLOW
    username:myUser
    password:myPassword
    grant_type:password
    scope:offline_access
    resource:resource_server
  2. Call resource api setting Authorization header in this way:
    Authorization:Bearer [TOKEN]
@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Aug 22, 2017

@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.

@michaelsogos

This comment has been minimized.

Copy link

michaelsogos commented Aug 22, 2017

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.

@PinpointTownes

This comment has been minimized.

Copy link
Contributor

PinpointTownes commented Aug 22, 2017

Thanks for the detailed feedback, @michaelsogos 👍

@michaelsogos

This comment has been minimized.

Copy link

michaelsogos commented Aug 22, 2017

Can i suggest you to add the explanation on wiky or at least on readme.md?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment