IdentityServer / IdentityServer3

OpenID Connect Provider and OAuth 2.0 Authorization Server Framework for ASP.NET 4.x/Katana
https://identityserver.github.io/Documentation/
Apache License 2.0
2.01k stars 764 forks source link

'Anonymous' tokens #1953

Open dylanmorley opened 8 years ago

dylanmorley commented 8 years ago

We're working on an ecommerce site that is moving towards a micro service architecture, we need to integrate with 'shopping bag' and 'saved items' services, both of which can be accessed in either an authenticated or unauthenticated state. For example, the customer may save some items, switch them all to bag, then checkout as a guest.

The problem

We want to apply bearer token validation to our APIs so that we have a standard, consistent authentication mechanism - but how should this work when the customer has not signed in? In the above scenario, we're never prompting the user for credentials.

We don't want to maintain multiple endpoints and have logic about which one to call, we want a single endpoint, secured by bearer token, and the client calls that regardless of their authentication state. So, how do they identify themselves uniquely so they can present a token to an API?

Acquire an anonymous token

We're writing a javascript SDK that we'll drop on relying party pages - it will make use of the Identity Server oidc to determine if the user is authenticated by polling the STS in the normal manner. If they aren't, then we'll make a request for a token and pass in a new scope anon

This would be passed through into DefaultTokenService which would determine that it should issue an anonymous token. We're proposing that this will have a standard set of claims and look something like this

Image of Anon Token

amr = anon did = device ID, a GUID generated and also applied to the subject

This can then be used to call APIs that are secured by bearer token, and logic within those APIs can check for the amr claim and take appropriate action.

I hacked in a changeset last night just for proof of concept, and will be working on this today to refine - these are the touchpoints I can see.

https://github.com/dylanmorley/IdentityServer3/commit/de073d7b10a542338c211bfe8f6e6404931afb59

@brockallen @leastprivilege - Any thoughts on this - do you agree it's a valid use case? Any security concerns you can see?

We researched AWS cognito when attempting to spec this out - http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.htm and have followed the same pattern regarding amr claim and GUID for identification.

Would you consider bringing 'anon' functionality into the main branch if you're happy with the work we do, or should we branch away?

Thanks

dylanmorley commented 8 years ago

Working version now available on my fork - We went with allow_anonymous as the new scope,

To test, you can call from the Javascript Implicit client - Token Manager. Add another button with attributes data-scope='openid profile allow_anonymous' data-type='id_token token'

When you're not authenticated, you'll get the anonymous token back. When you are signed in, you'll get the authenticated token back.

leastprivilege commented 8 years ago

thanks! We will look into it. Might take a couple of days because I am travelling next week.

dylanmorley commented 8 years ago

No probs on timings, whenever you get a spare moment,

Some notes on how I expect this to work when you're reviewing,

That's pretty much it it terms of flow - let me know if any questions.,

Thanks

leastprivilege commented 8 years ago

So it seems this all can be implemented using the standard extensibility points - why does that need to be a built-in feature?

dylanmorley commented 8 years ago

I couldn't see an obvious way of doing it - since we're changing the flow in AuthorizeEndpointController to not redirect if user is not authenticated and allow_anonymous scope provided

Would we need to provide our own implementation of AuthorizeEndpointController and expose that via /.well-known/openid-configuration?

However, is this a feature you think would be useful to contribute back? I've seen other people asking for similar functionality e.g. http://stackoverflow.com/questions/31184594/access-token-for-anonymous-users-jwt. Or is this too bespoke and you would prefer to keep it away from the core product?

leastprivilege commented 8 years ago

OK - let us discuss this internally.

MauricioArroyo commented 8 years ago

Hi @dylanmorley and @leastprivilege, I'll vote for this feature, we have pretty much the same scenario mentioned by dylan.

I thought about using the acr_values to send a new flag to generate the anonymous token in the PreAuthenticateAsync of my UserService, using the regular extension points.

My concern is when the user wants to login (after being an anonymous user for a while). Since the cookie is already generated with the server, I think it will go through without asking for credentials. I could use the prompt login to force it, but then it seems the regular scenario is not as clean as it should be...

Thanks for taking the time to consider this. @dylanmorley you did a great job on the explanation, couldn't be better! I'll be eagerly waiting for Dominick's comments.

leastprivilege commented 8 years ago

ok - give us a couple more days. (and thanks for being persistent ;)

leastprivilege commented 8 years ago

I think the combination of handling PreAuthenticate and prompt=login is pretty OK. Would you disagree?

MauricioArroyo commented 8 years ago

Well, I tried, but I most be doing something wrong because it keeps getting in the PreAuthenticate in a loop. I kept it in stand by waiting for new over here :)

I am going to take a look at it now and let you know. My guess is that since my guest user doesn't exists in my user service, methods such as IsActive are failing and/or returning false and that's creating the loop.

Let me try it and the comment on this option again.

leastprivilege commented 8 years ago

Sure - all methods would need to special case the "anonymous" amr.

leastprivilege commented 8 years ago

..and I would rather use an acr_value than a scope to signal the anonymous login.

dylanmorley commented 8 years ago

OK - thanks - let me look into this

dylanmorley commented 8 years ago

@leastprivilege so I'll make the change so that the request is made with the acr_value and the response includes the acr claim 'level 0' as per spec - http://openid.net/specs/openid-connect-core-1_0.html#IDToken.

"acr": "0" (rather than a URI \ Name)

Are you happy with acr_value as allow_anonymous?

leastprivilege commented 8 years ago

sure - thats up to you.

MauricioArroyo commented 8 years ago

@dylanmorley sorry I hijacked part of your question :(

I tried the acr_value send as part of the authentication request and handling it with the PreAuthenticate extension point and it worked just fine.

I have the special amr case on the IsActiveAsync and GetProfileDataAsync methods, basically checking if the ctx.Subject was a guest user and returning a fixed set of claims (or true in the case of IsActiveAsync).

A combination of what @dylanmorley made of the new AnonymousClaimsProvider and the regular extension points works fine.

@leastprivilege Thank you, as always you and Brock Allen are life savers :) And thank you again @dylanmorley!

jeremy001181 commented 8 years ago

what if I got a wsfed client, how can i force customer to login from there? would prompt login work?

MauricioArroyo commented 8 years ago

Hi @jeremy001181, If I understand correctly your question, your wsfed will be just another provider (the same way google and facebook, or ADFS). So when you login in as a anonymous user, you don't go to any of these identity providers.

Later, when you use the prompt login, basically you are forcing the IdSrv to show the login page (where you have Google, Facebook, your wsfed server)... so I would say it will work.

Now, if you want the prompt login go to your wsfed directly, I think you are looking for the idp acr_value, you can use that together with prompt login and it should work.

I may be missing some point of your question, please elaborate if I am.

Good day!

leastprivilege commented 8 years ago

wsfed does not have an equivalent feature to prompt=login. That's OIDC only.

jeremy001181 commented 8 years ago

@MauricioArroyo no, sorry I didn't make it clear, I use IdentityServer3.WsFederation plugin with my identityserver3 to support my wsfed clients. i understand how prompt login works, and i've got it working with oidc clients but i also have some client apps using wsfed, now i am getting a challenge how can i force anonymous users to login when they go there.

@leastprivilege thanks, i found there is a SignInValidator in wsfed plugin currently registered with Autofac using AsSelf (not interface), it doesn't have a interface otherwise i could inject my own one and force login from there?

dylanmorley commented 8 years ago

Hi,

We've got this working using the suggestion of PreAuthenticateAsync and a number of other customisations. Because we are supporting ws-fed clients as well as Open ID, we've had to make a change to the ws-fed plug in to check for the anonymous authentication state (amr == 0) and always show login in this case. However, we're deprecating support for ws-fed over the next year, so are happier to make code changes in the plugin rather than the main repository

In our web application, we also required another authorization attribute to bounce you to the login screen if 'amr = 0' value is found in the claims. I'll share our solution when it's finalised

The only thing I'm concerned about is the redirect path when requesting anonymous tokens, which looks like this.

trafficbounce

I would prefer an anonymous request to the authorize endpoint responds with the token directly, rather than the bounce to login and back to authorize. We're writing a javascript sdk to go with this, so obviously that has ajax implications - we're also a very high volume site with millions of unique visitors per day, so traffic paths need to be carefully considered.

We're going to performance test what we've got, but we may need to make amendments to the Authorize controller as originally spiked. Will post back what we finally go with.

orchidee11 commented 7 years ago

hello, Can you please post the sample

MauricioArroyo commented 7 years ago

Hi @Llamyae, Sorry I didn't answer before. What's is exactly what you need? I will be glad to help.

Mauricio

orchidee11 commented 7 years ago

@MauricioArroyo actually I'm trying to give access to anonymous user to certain resources in my web app I tried to edit the PreAuthenticateAsync method to prevent the login page from displaying but no success

MauricioArroyo commented 7 years ago

I see. This is what I did. I sent an acr value as part of the request, this acr values is a flag indicating that the user I am trying to login is not authenticated and I want her to be a guest. In my case I am adding a guid as part of that acr indicating what's the "username" for that guest. On the PreAuthenticateAsync if I find the acr I call an internal method that basically returns a AuthenticateResult, with a made up username, and a few claims that I need all the users to have in order to use some part of the system. What have you tried? If you give me more context I could be more helpful. :)

MauricioArroyo commented 7 years ago

When I get to my computer, I can give you some snippets.

orchidee11 commented 7 years ago

Up

On Sat, Nov 26, 2016 at 3:35 PM, MauricioArroyo notifications@github.com wrote:

When I get to my computer, I can give you some snippets.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/IdentityServer/IdentityServer3/issues/1953#issuecomment-263084236, or mute the thread https://github.com/notifications/unsubscribe-auth/AILla5Isva1kZxviWEWEBJQqN9tU16ETks5rCJghgaJpZM4GDrH7 .

MauricioArroyo commented 7 years ago

Hey, sorry I forgot... too busy on the weekend... up means you figure it out?

orchidee11 commented 7 years ago

no I'm waiting for your response .

On Mon, Nov 28, 2016 at 3:25 PM, MauricioArroyo notifications@github.com wrote:

Hey, sorry I forgot... too busy on the weekend... up means you figure it out?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/IdentityServer/IdentityServer3/issues/1953#issuecomment-263384040, or mute the thread https://github.com/notifications/unsubscribe-auth/AILla2oiFq4AozVvHzeOVIK4evGiE-jVks5rCzjTgaJpZM4GDrH7 .

MauricioArroyo commented 7 years ago

Oh, sorry. Ok here it goes. Where I am in a place that needs the user to be authenticated and falls back to the guest user. Basically I do two things (if the user is not already authenticated of course):

  1. Add to the OwinContext a flag indicating I would need a guest user authentication. In my case, I send the Guid that I use for the guest cookie. Request.GetOwinContext().Set("NAME OF YOUR CHOOSING", guestId.ToString());
  2. Return from the action (I am using MVC) a Challenge result return new ChallengeResult("OpenIdConnect", url);

On the Startup, under OpenIdConnectAuthenticationNotifications, I catch if the flag was set, on the RedirectToIdentityProvider. if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest){ if the flag is set, I add the following to the message ProtocolMessage.AcrValues = $"{flagId}:{guestId}"; }

After than on the IdentityServer PreAuthenticateAsync, I check if the AcrValue exists, If it does, I do the guest authentication, that's straight forward. public override async Task PreAuthenticateAsync(PreAuthenticationContext context) { if the Acr for guest is part of context.SignInMessage.AcrValues return AuthenticateResult with the values for your guest use. In our case, I use the guestId to generate a fake username }

I hope this helps. Just fill in the blanks with your own ids and you are set to go!

orchidee11 commented 7 years ago

Thank you, I'll try this and let you know.

On Mon, Nov 28, 2016 at 3:43 PM, MauricioArroyo notifications@github.com wrote:

Oh, sorry. Ok here it goes. Where I am in a place that needs the user to be authenticated and falls back to the guest user. Basically I do two things (if the user is not already authenticated of course):

  1. Add to the OwinContext a flag indicating I would need a guest user authentication. In my case, I send the Guid that I use for the guest cookie. Request.GetOwinContext().Set("NAME OF YOUR CHOOSING", guestId.ToString());
  2. Return from the action (I am using MVC) a Challenge result return new ChallengeResult("OpenIdConnect", url);

On the Startup, under OpenIdConnectAuthenticationNotifications, I catch if the flag was set, on the RedirectToIdentityProvider. if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType. AuthenticationRequest){ if the flag is set, I add the following to the message ProtocolMessage.AcrValues = $"{flagId}:{guestId}"; }

After than on the IdentityServer PreAuthenticateAsync, I check if the AcrValue exists, If it does, I do the guest authentication, that's straight forward. public override async Task PreAuthenticateAsync(PreAuthenticationContext context) { if the Acr for guest is part of context.SignInMessage.AcrValues return AuthenticateResult with the values for your guest use. In our case, I use the guestId to generate a fake username }

I hope this helps. Just fill in the blanks with your own ids and you are set to go!

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/IdentityServer/IdentityServer3/issues/1953#issuecomment-263388331, or mute the thread https://github.com/notifications/unsubscribe-auth/AILla7xVI23mlMcR6xJGgCrP6KSL5wOtks5rCz0BgaJpZM4GDrH7 .

orchidee11 commented 7 years ago

sorry for bothering you again,I have few questions,

do you generate a random new Guid everytime ? and where in your code you place this method,if it's possible can you share the code

On Mon, Nov 28, 2016 at 3:50 PM, wrote:

Thank you, I'll try this and let you know.

On Mon, Nov 28, 2016 at 3:43 PM, MauricioArroyo notifications@github.com wrote:

Oh, sorry. Ok here it goes. Where I am in a place that needs the user to be authenticated and falls back to the guest user. Basically I do two things (if the user is not already authenticated of course):

  1. Add to the OwinContext a flag indicating I would need a guest user authentication. In my case, I send the Guid that I use for the guest cookie. Request.GetOwinContext().Set("NAME OF YOUR CHOOSING", guestId.ToString());
  2. Return from the action (I am using MVC) a Challenge result return new ChallengeResult("OpenIdConnect", url);

On the Startup, under OpenIdConnectAuthenticationNotifications, I catch if the flag was set, on the RedirectToIdentityProvider. if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authe nticationRequest){ if the flag is set, I add the following to the message ProtocolMessage.AcrValues = $"{flagId}:{guestId}"; }

After than on the IdentityServer PreAuthenticateAsync, I check if the AcrValue exists, If it does, I do the guest authentication, that's straight forward. public override async Task PreAuthenticateAsync(PreAuthenticationContext context) { if the Acr for guest is part of context.SignInMessage.AcrValues return AuthenticateResult with the values for your guest use. In our case, I use the guestId to generate a fake username }

I hope this helps. Just fill in the blanks with your own ids and you are set to go!

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/IdentityServer/IdentityServer3/issues/1953#issuecomment-263388331, or mute the thread https://github.com/notifications/unsubscribe-auth/AILla7xVI23mlMcR6xJGgCrP6KSL5wOtks5rCz0BgaJpZM4GDrH7 .

MauricioArroyo commented 7 years ago

The guid gets saved in a "guest" cookie, so it doesn't change for that user in that particular machine. So every time I need to log in as guest, I use the cookie to extract the Guid. The place where you create the cookie will be up to you, but I would recommend when you need it to send the request on the Identity Server, create it if it doesn't exists, and it does exist, just take out the guid you need.

MauricioArroyo commented 7 years ago

did it work?

orchidee11 commented 7 years ago

no it didn't . I have problem editting this line doesn't recognize the guestID , itProtocolMessage.AcrValues = $"{flagId}:{guestId}"

MauricioArroyo commented 7 years ago

I am not quite getting what you mean. Could you please post the snippet. GuestId is a variable.

orchidee11 commented 7 years ago

I created a guest cookie like you suggestes, and in the startup file I added this method ` RedirectToIdentityProvider = n => {

  if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest)
                    { 

                            n.ProtocolMessage.AcrValues = $"{flagId}:{GuestID}";

                    }`
MauricioArroyo commented 7 years ago

What do flagID and GuestID variables have? You need to take the GuestID out of the OwinContext.

orchidee11 commented 7 years ago

like this var id = n.OwinContext.Get<int>(GuestID);

MauricioArroyo commented 7 years ago

You have to make sure you included that in the context. If your id is an int, then I guess it's ok, but if it's a Guid, then it's not. Then if you have the variable id, you need to change the {GuestID} for {id} in the string interpolation.

orchidee11 commented 7 years ago

it's a GUID , so how to do that

MauricioArroyo commented 7 years ago

Not sure what you mean. You can't get a guid as int, you know that right? save the guid in the context as string and then retrieve it as string too... if I am understanding your question correctly.

orchidee11 commented 7 years ago

I'm not sure I get it right
var GuestID = n.OwinContext.Get("GuestID"); if (GuestID != null) { n.ProtocolMessage.AcrValues = GuestID; }

wen I run the app the GuestID is always null !

MauricioArroyo commented 7 years ago

Sorry I didn't answer before. I was out of town. Are you setting the value in the OwinContext before with the same key?

orchidee11 commented 7 years ago

yes I did it works, but the login page keeps displaying it's like the PreAuthenticateAsync never get executed even though I specefied it in the startup file like this factory.UserService = new Registration<IUserService, TestUserService>();

MauricioArroyo commented 7 years ago

hmm, the PreAuthenticateAsync method not being executing is weird, it should if you challenge the authentication even without the flags, so that's not an issue of this change, you will need to check the configuration of the site... is it configured to use the IdSvr?

orchidee11 commented 7 years ago

yes it is

MauricioArroyo commented 7 years ago

Create the smallest project you can with your configuration and post it so I can take a look. Would it be possible?

orchidee11 commented 7 years ago

ok i will thank you

orchidee11 commented 7 years ago

here it is

https://www.dropbox.com/s/nlyrh4zcyv60cnc/MVCAuthenticationAuthorization.rar?dl=0