Connect OAuth Tokens with a user - c#

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.

Related

Load roles from database based in JWT's userId

I'm trying to implement some database fetching in my JWT authentication, so that I can dinamically fetch the roles and other info from the database, I would ideally only want to store the userId in the JWT payload.
Doing that, I can do stuff like store the date where the password was last changed, and refuse the token in case it was issued before that.
The problem is that I have no idea where on the chain I would do such thing, I'm just getting started with .NET/ASP.NET
I have this on my ConfigureServices currently:
var jwtKey = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
ValidateIssuer = false,
ValidateAudience = false,
};
});
And this on my Configure:
app.UseAuthentication();
app.UseAuthorization();
With that, I can access some user info inside a controller's action with User.Identity.Name for example, how would I do that after loading the model from the DB?
REQUIREMENTS
These are usually the main things you will care about:
APIs must have access to domain specific claims so that they can authorize correctly
Tokens returned to internet clients are kept confidential
OPTION 1
The optimal way to meet the requirements is to issue opaque tokens to internet clients, then introspect them before they reach the API, as described in the Phantom Token Approach.
An Authorization Server can then reach out to your domain specific data at the time of token issuance, to include custom claims in access tokens.
OPTION 2
In the API code there are two stages involved in integrating custom claims, and the second of these can be customized:
Verify the JWT access token
Form a ClaimsPrincipal
If your Authorization Server doesn't support opaque access tokens, another option that will work is a Custom Authentication Handler. It adds some complexity to your API though. See this blog post of mine and this code.

How to configure the OAuth callback to a different domain in ASP.NET Core authentication

I am Authenticating against an OAuth endpoint where I can only configure 1 callback domain. (and localhost is whitelisted).
I have my web app running in Azure (myapp.azurewebsites.net) and have it available with two custom domains (myapp.cc and myapp.eu). When I use the default setup, the CallbackPath can only be a relative path (to the current domain)
The code documentation of CallbackPath indicates it's relative to the application's base path:
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// </summary>
public PathString CallbackPath { get; set; }
I want to make sure the CallBack happens to the (only) domain that I whitelisted on the OAuth backend. I know I can implement everything manually, but I was hoping there would be an easy way to work around this design and still benefit from the baked in Authentication options.
So even if a user is logging on on the myapp.cc or the myapp.eu or the myapp.azurewebsites.net , it should redirect to myapp.azurewebsites.net/ (which is whitelisted on my Auth service)
A part of my Startup.cs file is pasted below:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", "MyService",
options =>
{
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientOauthSecret;
options.CallbackPath = "/relativeonlypath";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
/// ... removed for brevity
}
);
Any idea on how to implement this?
Thank you
I'm not sure it's possible, because to verify that the user is redirected to your application as part of a "genuine" authentication flow, the ASP.NET OAuth handler performs the following steps:
Before redirecting the user to the OAuth service, ASP.NET Core generates a "correlation" cookie that is tied to the current domain; and
When the user is redirected to the app, the handler looks for this cookie and validates its content.
So if the correlation cookie is generated in step #1 for one domain, let's say myapp.cc, and the user is redirected to another domain, myapp.azurewebsites.net, ASP.NET Core might not be able to read it because the browser will not have included it in the redirection request.
Note
As seen in the first comments, the original thought was to leverage the SameSiteproperty of the correlation cookie to have it sent by the browser to the second domain.
This was all wrong, apologies!
I now think that you have 2 different options:
Redirect every request from myapp.cc and myapp.eu to myapp.azurewebsites.net, so that when the authentication flow happens, we're already on the right domain; or
Redirect the user to the myapp.azurewebsites.net domain before redirecting them to the OAuth server.
I won't go into the first solution, as there's plenty of ways to achieve this.
Here's some code that I haven't tested that could work for the second solution:
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", options =>
{
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
var currentRequestUri = new Uri(context.Request.GetDisplayUrl());
// 1. If we're not on the correct domain, redirect the user to the same page, but on the expected domain.
// The assumption is that the authentication flow will also kick in on the other domain (see 2).
if (currentRequestUri.Host != "myapp.azurewebsites.net")
{
var applicationRedirectUri = new UriBuilder(currentRequestUri)
{
Host = "myapp.azurewebsites.net"
}.Uri.ToString();
context.Response.Redirect(applicationRedirectUri);
return Task.CompletedTask;
}
// 2. If we reach here, it means we're on the right domain, so we can redirect to the OAuth server.
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

Strange behaviour when accepting Bearer authentication and OpenIdConnect from a single client with IdentityServer4

I am having some issues after making some tweaks to an IdentityServer4 Quickstart sample solution, specifically the 8_AspNetIdentity sample.
I'll preface this by saying I'm not sure if what I'm trying to do is just not supported, or if I'm doing it wrong.
This sample solution contains the following projects relevant to my question:
an IdentityServer,
an MVC client (named MVCClient) that uses OpenIdConnect to authenticate its users,
a web API client (named API) that uses bearer authentication for its users
a console app (named ResourceOwnerClient) designed to be a client of the API
What I am trying to do is merge the API project into the MVCClient, so that the MVCClient could both authenticate the users from its MVC website with OIDC, and also the ResourceOwnerClient using bearer authentication.
I made the following changes to the MVCClient's Startup.cs:
changed services.AddMvc(); to:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder(new[]
{
JwtBearerDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
"oidc"
})
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
added JWT bearer options to the services.AddAuthentication():
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
})
Now technically this did work, as both the ResourceOwnerClient and the MVC users can successfully authenticate with the MVCClient. I however have one caveat:
When I authenticate with a user from the MVC side, I noticed that there are two identities in my current User. Both are identical in terms of claims, etc. This only happens when I put a breakpoint in the MVCClient, on the IdentityServer there is only one identity.
On the IdentityServer, I have registered a UserClaimsPrincipalFactory which adds my own custom claims to the ClaimsIdentity. In the two identities on the IdentityServer, I can see the claims duplicated. So instead of having one identity with two custom claims, I see two identities which each have 4 custom claims. The CreateAsync method in my UserClaimsPrincipalFactory is also getting hit 5 times for a single login.
Although this behaviour is strange, it does not seem to be having any negative impacts. But this is only a proof of concept for a larger application that I'm building, and I'm afraid I may run into issues in the future because of it.
If anyone has attempted this sort of thing before, or knows why this behaviour could be happening, any help would be appreciated.
While nothing bad should happen with this design, I would completely remake it. Why? Because you are mixing a Client and an ApiResource, and they should be logically separated. A Client is an application, something some user interacts with, even if it was a headless one (i.e an automated service); while an ApiResource consists of resources that are provided to Clients, so no user can interact with it directly.
You could add two authentications against IdentityServer, one as API (and add it as JwtBearer) and one as a Client (and add it as Cookies). You can then use [Authorize(AuthenticationSchemes = "JwtBearer")] and = "Cookies" depending on the function of that Action/Controller.
Leaving that aside, the problem is that your application is getting one Identity for the MVC side and one for the API side, since it has no way of telling which one you want.
Just so you have an idea, this is how one of my IdentityServers with ASP.NET Core Identtiy look like, where you can login against it using the UI and also hit the REST endpoints with a JwtToken:
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["IdentityServerUrl"];
options.ApiName = Configuration["ApiName"];
options.RequireHttpsMetadata = false;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
});

ASP.Net Core MVC/API/SignalR - Change authentication schemes (Cookie & JWT)

I've a .Net Core 2.2 web application MVC in which I've added API controllers and SignalR hubs. On the other side, I've a mobile app that calls the hub methods. Before calling hubs from the app, I am authenticating my users through an API call - getting back a JWT Token - and using this token for future requests, this way I can use Context.User.Identity.Name in my hub methods:
public static async Task<string> GetValidToken(string userName, string password)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_API_BASE_URI);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
LoginViewModel loginVM = new LoginViewModel() { Email = userName, Password = password, RememberMe = false };
var formContent = Newtonsoft.Json.JsonConvert.SerializeObject(loginVM);
var content = new StringContent(formContent, Encoding.UTF8, "application/json");
HttpResponseMessage responseMessage;
try
{
responseMessage = await client.PostAsync("/api/user/authenticate", content);
var responseJson = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); ;
var jObject = JObject.Parse(responseJson);
_TOKEN = jObject.GetValue("token").ToString();
return _TOKEN;
}catch
[...]
Then using the token:
_connection = new HubConnectionBuilder().WithUrl(ApiCommunication._API_BASE_URI + "/network", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
}).Build();
So far so good. It's working as expected on my mobile app. But in order to make it work I had to set this piece of code on server side (Startup.cs):
services.AddAuthentication(options =>
{
options .DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options .DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
...
This prevents me for using cookie authentication anymore and therefore the mvc web app is no more working as expected as it's not able to get the current authenticated user amongs requests.
Removing the lines:
options .DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options .DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
makes the web app working correctly but not the mobile app anymore (hub calls fail due to Context.User.Identity.Name equals to null).
I've been searching all around about how to handle different schemes (in my case cookie + jwt) and from my understanding, this is by design not possible anymore.
Is there any possible workaround to use double scheme or am I missing something?
I thought maybe I shoud host 2 separate projects instead and use one with Cookie authentication and the other one with JWT?
Thanks in advance.
There are multiple ways to solve the issue you encounter, but first let's go through why it's not currently working.
What DefaultAuthenticateScheme means
When you set a value to the DefaultAuthenticateScheme property of AuthenticationOptions, you instruct the authentication middleware to try and authenticate every single HTTP request against that specific scheme. I'm going to assume that you're using ASP.NET Identity for cookie-based authentication, and when you call AddIdentity, it registers the cookie authentication scheme as the default one for authentication purposes; you can see this in the source code on GitHub.
However, it doesn't mean you can't use any other authentication scheme in your application.
The authorization system default policy
If all the protected endpoints of your application are meant to be accessible to clients authenticated with cookies or JWTs, one option is to use the authorization system default policy. That special policy is used when you use "empty" instances of the AuthorizeAttribute class — either as an attribute to decorate controllers/actions, or globally at the app level with a new AuthorizeFilter(new AuthorizeAttribute()).
The default policy is set to only require an authenticated user, but doesn't define which authentication schemes need to be "tried" to authenticate the request. The result is that it relies on the authentication process already having been performed. It explains the behavior you're experiencing where only one of the 2 schemes works at a time.
We can change the default policy with a bit of code:
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-cookie-authentication-scheme", "your-jwt-authentication-scheme")
.Build();
})
Specific authorization policies
If you find yourself in a situation where you require some endpoints to only be accessible to clients authenticated with cookies and others with JWTs, you can take advantage of authorization policies.
They work exactly like the default policy, expect you get to pick on an endpoint basis which one applies. You can add policies like so:
services.AddAuthorization(options =>
{
options.AddPolicy("Cookies", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-cookie-authentication-scheme")
.Build());
options.AddPolicy("JWT", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("<your-jwt-authentication-scheme")
.Build());
})
You can then refer to these policies in appropriate endpoints by decorating them with [Authorize(Policy = "<policy-name>")]. As a side note, if the only differentiator between your policies is the authentication scheme, it's possible to achieve the same result without creating policies, and referring to the appropriate authentication scheme(s) in [Authorize] attributes with the AuthenticationSchemes property.
Policies are valuable when you have more complex rules, like that specific claim needs this specific value, for example.
I hope this helps, let me know how you go! 👍

User is authenticated but where is the access token?

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

Categories