User is authenticated but where is the access token? - c#

I have a web Application which authenticates a user to an Identity Server 4, using an implicit client. I need the access token for this user so that I can make a call to another API.
To be clear:
I have an identity Server. Created using Identity server 4.
I have the web app in question created in Asp .net core mvc.
API created in .net core.
The Web application authenticates the user against the identity server. Once they are authenticated we use bearer tokens to access the API.
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Configuration["ServiceSettings:IdentityServerEndpoint"];
options.ClientId = "f91ece52-81cf-4b7b-a296-26356f50841f";
options.SignInScheme = "cookie";
});
The user is authenticating fine and i am able to access the controller below. I need an access token for this user so that i can make a request to another API.
[Authorize]
public async Task<IActionResult> Index(int clientId, string error)
{
ViewData["Title"] = "Secrets";
if (User.Identity.IsAuthenticated)
{
// All of the below attempts result in either null or empty array
var attempt1 = Request.Headers["Authorization"];
var attempt2 = await HttpContext.GetTokenAsync("access_token");
var attempt3 = _httpContextAccessor.HttpContext.Request.Headers["Authorization"];
var attempt4 = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
}
return View();
}
The following does contain a header called cookie. Is there a way of getting the access token out of that?
var h = _httpContextAccessor.HttpContext.Request.Headers.ToList();
How can i find an access token for the current authenticated user? Using Implicit login.
Note on Hybrid vs implicit login: I cant use hybrid login due to the issue posted here Authentication limit extensive header size As i have not been able to find a solution to that problem a suggestion was to switch to an implicit login rather than hybrid. Implicit does not appear to create the giant cooking the hybrid did.
I have been following this to create the implicit client Getting started with Identityserver 4

By default the OpenID Connect middleware only requests an identity token (a response_type of id_token).
You'll need to first update your OpenIdConnectOptions with the following:
options.ResponseType = "id_token token";
You can then save the tokens to your cookie using:
options.SaveTokens = true;
And then finally, you can access the token using:
await HttpContext.GetTokenAsync("access_token");
Note that you will also need to set the AllowAccessTokensViaBrowser flag in your IdentityServer client configuration when using the implicit flow.

Use options.SaveTokens = true
then grab your access token from the claims or use HttpContext.GetTokenAsync
here's the link to the blogpost with example: https://www.jerriepelser.com/blog/accessing-tokens-aspnet-core-2/

I solved using the IHttpContextAccessor:
var token = _accessor.HttpContext.Request.Headers["Authorization"];
return token.ToString().Replace("Bearer ", string.Empty);

Related

WebAPI making an extra trip for user claims using OIDC authentication handler

My Current Setup is:
I have an Identity server built using Duenede.IdentityServer package running at port 7025.
I have a WebApi which is Dotnet 6 based and below is its OIDC configuration.
AddOpenIdConnect("oidc", o =>
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
o.SaveTokens = true;
o.GetClaimsFromUserInfoEndpoint = true;
o.RequireHttpsMetadata = false;
o.ResponseType = "code";
o.Authority = "https://localhost:7025/";
o.ClientId = "some clientid";
o.ClientSecret = "some secret";
o.Scope.Clear();
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("dotnetapi");
o.NonceCookie.SameSite = SameSiteMode.Unspecified;
o.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
o.ClaimActions.MapUniqueJsonKey("role", "role");
o.ClaimActions.MapUniqueJsonKey("email", "email");
});
Now when web api request the token from the identityserver (OIDC is the challenge scheme and i have a cookie scheme set as default authentication scheme) it gets both id_token and access_token(verified using await HttpContext.GetTokenAsync("access_token"); await HttpContext.GetTokenAsync("id_token");). I can also find user claims in HttpContext.User.FindFirst("some claim");
But i have noticed that there is an extra call to the identity server from web api for the userinfo. I observed that it may be because of o.GetClaimsFromUserInfoEndpoint = true; when i omitted this line i found that user claims are not set, even though i am still getting both id and access token.
So my understanding is the OIDC client of dotnet is using userinfo endpoint to fetch the user claims. But my question is if i am already receiving the access_token why there is an extra call for the userinfo. Can this extra call be prevented?
is there any way so that i receive id_token at first and access_token is then fetched as it is doing now so that same information is not sent twice?
First, you can set this client config in IdentityServer to always include the user claims in the ID token
AlwaysIncludeUserClaimsInIdToken
When requesting both an id token and access token, should the user
claims always be added to the id token instead of requiring the client
to use the userinfo endpoint. Default is false.
The reason for not including it in the ID-token is that increases the size of the id-token and if you store the tokens in the asp.net session cookie, it also can become pretty big.
I wouldn't worry about the extra request that happens when the user authenticates.

How can I get a refresh-token from Google OAUTH in ASP.Net Core 5 Identity?

How do you get a refresh-token from Google in ASP.Net Core Identity 5?
I am able to get an access-token, but not a refresh-token.
Startup.cs, ConfigureServices
...
services.AddAuthentication()
.AddGoogle(options =>
{
IConfigurationSection googleAuthNSection = Configuration.GetSection("Authentication:Google");
options.ClientId = googleAuthNSection["ClientId"];
options.ClientSecret = googleAuthNSection["ClientSecret"];
options.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
options.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
options.Scope.Add("https://www.googleapis.com/auth/calendar");
//this should enable a refresh-token, or so I believe
options.AccessType = "offline";
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
When I sign up with a google account and the code hits "OnCreatingTicket", I get an access token - but no refresh-token...:
Question
What am I missing to get a refresh-token back here?
The code was actually working just fine.
However, you only get a refresh-token back from Google the first time you register with a new account for the specific OAuth 2.0 Client Id. I was deleting my local user data and signing up again - but this does not make google send the refresh-token again - only access token.
If you want to use the refresh-token offline, you also need to store it somewhere (like in the database) yourself - this does not happen with the above code.
I might be a bit late to this, but found out that you can put this line in the options to force the refresh token. (Its a bit of a hack mind you)
options.AuthorizationEndpoint += "?prompt=consent";

Connect OAuth Tokens with a user

I'm trying to figure out the best way to map tokens to a user. I think I've falling across a common problem Authorization vs Authentication.
I'm creating a market place which, my payments service is backed by stripe so I allow logins using stripe currently.
I register my stripe service like so:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddOAuth<OAuthOptions, StripeConnectOAuthHandler<OAuthOptions>>(
StripeConnectDefaults.AuthenticationScheme, options => {
options.SaveTokens = true;
options.ClientId = Configuration["Stripe:ClientId"];
options.ClientSecret = Configuration["Stripe:ClientSecret"];
options.TokenEndpoint = StripeConnectDefaults.TokenEndpoint;
options.AuthorizationEndpoint = StripeConnectDefaults.AuthorizationEndpoint;
options.UserInformationEndpoint = StripeConnectDefaults.UserInformationEndpoint;
options.Scope.Add("read_write");
options.CallbackPath = new PathString("/signin-stripeconnect");
//...
});
Since stripe is what I use to handle the payments I need the token to perform certain behavior like creating a pay event or subscribing to one but I don't want to enforce that my users must have a stripe account to view data on my site.
So I'd like to add additional ways to login, but I need to link these users together
app.UseGoogleAuthentication(new GoogleOptions()
{
AuthenticationScheme = "Google",
DisplayName = "Google",
SignInScheme = COOKIE_AUTH,
ClientId = "sdlfkjgsdlkfjgsdf-sdfadsfasdf.apps.googleusercontent.com",
ClientSecret = "myClientSecretBase64==",
});
However if I do this, I need a way to link my google login and my stripe account login. Prior to now I was using IdentityServer4. Generally Token Servers are sperate from the API. So It seems a bit of overkill to host a token server, if only a single application is going to consume it.
Is there a simple way allow authentication, while still giving the ability to connect to external api's such as stripe?
Note: If the solution requires IdentityServer 4 I don't mind, I just would rather not having to host 2 seperate applications
Hmm, this seems to be caused by a misconception of mine about Identity. Identity already a single user to have different login methods. In my authentication controller I fixed this by attaching an external login to an existing user if the user already exist.
var user = await this._userManager.FindByEmailAsync(email);
if(null == user)
{
user = await this.CreateIdentityUser(info, email);
}
var addLoginResult = await _userManager.AddLoginAsync(user, info);
Warning: You shouldn't be careful linking users, if you irregardlessly link users based on email, you could run into an issue where another user creates a fake account on the provider using the same email.

How do I call a web API on behalf of an AAD user using security claims?

So, to give you a general overview of what I am trying to achieve, I have a web application which
uses AAD authentication, and so users need to be signed in to a Microsoft organizational account in
order to use most of the controllers implemented in the web app (which targets .NET Core).
Visual Studio offers a template for this kind of web app setup. This
template project seems to obtain the user’s identity as a "ClaimsIdentity" (System.Security.Claims.ClaimsIdentity), which is ok so far, as long
as the user is AAD authenticated.
I also have a .NET Core Web API solution which the web app needs to make calls to on behalf of the
logged in user. So, I have a web app which signs in the user to AAD and then a web API (which the
web app calls) which has controller end points that expect an AAD authenticated request.
For this to work, my understanding is that the web app needs to include the signed in identity that
Microsoft, (which in this case is the security provider) provided it with, inside the header of the
request that it makes to the API. The API would then be able to view user claims and act accordingly.
The problem is here. As a header, I believe I need to provide the access token that Microsoft sends
to the web app.. however I cannot locate this token. All I can extract from User or User.Identity, are the claims. How
can I call a separate API on behalf of these claims? Do I need to completely disregard the template
that Microsoft provided and just make a call to the /token endpoint? I would just like to do this the right way :)
This is the ConfigureServices method in the web app Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
This is where I would like to call the external web API on behalf of the logged in AAD to get the
required data:
public IActionResult Index()
{
var user = User.Identity as ClaimsIdentity;
var request = (HttpWebRequest)WebRequest.Create("http://localhost:4110/data");
request.Headers["Authorization"] = "bearer " + getAccessToken_using_user;
var response = (HttpWebResponse)request.GetResponse();
var dataString = new StreamReader(response.GetResponseStream()).ReadToEnd();
return View();
}
Of course, my intention is to replace "getAccessToken_using_user" with the access token that Microsoft supposedly provides the web app with, as illustrated in their diagram.
You can use MSAL to get the access token for the downstream API.
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/on-behalf-of#practical-usage-of-obo-in-an-aspnet--aspnet-core-application
This is a full example with on behalf flow:
https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/2.%20Web%20API%20now%20calls%20Microsoft%20Graph
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes)
{
...
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Events.OnTokenValidated = async context =>
{
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
context.Success();
// Adds the token to the cache, and also handles the incremental consent and claim challenges
tokenAcquisition.AddAccountToCacheFromJwt(context, scopes);
await Task.FromResult(0);
};
});
return services;
}
private async Task GetTodoList(bool isAppStarting)
{
...
//
// Get an access token to call the To Do service.
//
AuthenticationResult result = null;
try
{
result = await _app.AcquireTokenSilent(Scopes, accounts.FirstOrDefault())
.ExecuteAsync()
.ConfigureAwait(false);
}
...
// Once the token has been returned by MSAL, add it to the http authorization header, before making the call to access the To Do list service.
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
// Call the To Do list service.
HttpResponseMessage response = await _httpClient.GetAsync(TodoListBaseAddress + "/api/todolist");
...
}

Why having cookies on token based authentication using Identity Server and asp.net core 2

I am creating a sample application to just to understand how identity server 4 authentication works with Asp.net core 2. I have noticed some cookies are generated for different levels as it can be seen in the attached screenshot. My problems is why these cookies are generated?
Below statement, I take it from the Identity Server document. When identity server is configuring
IdentityServer internally calls both AddAuthentication and AddCookie with a custom scheme (via the constant IdentityServerConstants.DefaultCookieAuthenticationScheme),
Here why it calls AddCookies method on identity server itself?
Also when I configure Asp.net core web client to use Identity server authentication it also call AddCookie() method. When I try to comment it It will give me an error. I am bit of unclear what is happening here.
Identity Server Configurations
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddToDoUserStore()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
services.AddAuthentication("MyCookie")
.AddCookie("MyCookie", options =>
{
options.ExpireTimeSpan = new System.TimeSpan(0, 0, 15);
});
Web Client Configuration
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://localhost:44377/";
options.RequireHttpsMetadata = true;
options.ClientId = "ToDoTaskManagmentClient";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("address");
options.Scope.Add("roles");
options.Scope.Add("usertodoapi");
options.Scope.Add("countries");
options.Scope.Add("subscriptionlevel");
options.Scope.Add("offline_access");
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.ClientSecret = "secret";
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Clear();
options.ClaimActions.MapJsonKey("given_name", "given_name");
options.ClaimActions.MapJsonKey("family_name", "family_name");
options.ClaimActions.MapJsonKey("role", "role");
options.ClaimActions.MapJsonKey("country", "country");
options.ClaimActions.MapJsonKey("subscriptionlevel", "subscriptionlevel");
options.Events = new OpenIdConnectEvents()
{
OnTokenValidated = e =>
{
var identity = e.Principal;
var subjectClaim = identity.Claims.FirstOrDefault(z => z.Type == "sub");
var expClaims = identity.Claims.FirstOrDefault(z => z.Type == "exp");
var newClaimsIdentity = new ClaimsIdentity(e.Scheme.Name);
newClaimsIdentity.AddClaim(subjectClaim);
newClaimsIdentity.AddClaim(expClaims);
e.Principal = new ClaimsPrincipal(newClaimsIdentity);
return Task.FromResult(0);
},
OnUserInformationReceived = e =>
{
e.User.Remove("address");
return Task.FromResult(0);
}
};
});
Your Identity Server application needs an authentication cookie (and session ID cookie) so that the front channel endpoints (authorize, consent, check_session_iframe and possibly others) know if the user is authenticated or not and the current state of the session. Without this it would have no idea who was calling it. IDS4 will automatically redirect to the login URL of the default scheme if it detects that the incoming request is not authenticated - you are then free to implement any authentication flow you like.
Your client applications may or may not need cookies depending on the architecture. A traditional server side WebForms or MVC-style app will need one but a pure JS client using a library like oidc-client-js will not and can talk to the back-end purely using the access token obtained from your identity server.
IdentityServer doesn't do any of this. All it does is handle the low-level authentication/authorization and return a claims principal. Your application that's using IdentityServer is the one that would set the cookie.
What you're doing here is essentially having the same app host both IdentityServer and a cookie auth-based frontend. The cookie portion is for the traditional login flow UI, so that the app can recognize whether the user is authenticated and redirect to a login form or to an account page or back to the originating app, if or when they are authenticated.
That piece could be completely spun-off into a totally different app, and then your IdentityServer app would no longer need the cookie auth config.

Categories