.net core 6 OpenID(Steam) authentication from API - c#

I'm trying to make it available for users to Authorize through Steam.
I'm currently working with a Frontend on port 8080 and a backend on port 5000. There is a local SSL on both services.
I'm able to sign in via Steam through the backend and retrieve information, the claims, about the user logged in. The problem is that the cookie or claims that is sent after the successful login is not available from the instance of the frontend, because when the user authorize with Steam it's entirely through the backend and therefore no WebToken, cookie or session is available to the frontend.
I'm using
AspNet.Security.OpenId (6.0.0)
AspNet.Security.OpenId.Steam (6.0.0)
I currently have the following setup in the backend
Startup.cs in ConfigureServices
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Authentication/SteamLogin";
options.LogoutPath = "/Authentication/SteamLogout";
options.Cookie.Domain = "https://localhost:8080"; //Tried to do some shared cookie thing but doesnt work
})
.AddSteam(options =>
{
options.ApplicationKey = "SECRET_APP_KEY";
});
AuthenticationController SteamAuthorize
So for connecting through steam you need to access this location directly
"https://localhost:5000/Authentication/SteamAuthorize".
Normally i would authenticate by sending a GET or POST call from the frontend so the frontend would have a relation to the backend. Here there is none because you access the action directly.
[AllowAnonymous]
[Route("SteamAuthorize")]
[HttpGet]
public IActionResult SteamAuthorize()
{
return Challenge(new AuthenticationProperties { RedirectUri = "/", IsPersistent = true }, "Steam");
}
AuthenticationController Index
So after the user has successfully logged in through steam the response is sent to this location.
In here it's not a problem getting the acquired data, but now i need the claims and identity to be available when my frontend makes a request to the backend.
[HttpGet("~/")]
public async Task<ActionResult> Index()
{
var openIdSteamId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
var steam_userName = User.Identity.Name;
if (steam_userName != null && openIdSteamId != null)
{
var steam_userId = openIdSteamId.Value.Replace("https://steamcommunity.com/openid/id/", "");
//I've tried redirecting to the frontend which then makes a new call afterwards
//The frontend will make a new call to this "/" page and now the Claims are gone
return Redirect("https://localhost:8080/steamauthorizing");
}
return Redirect("https://localhost:8080/");
}
I've tried to set options.Cookie.Domain = "https://localhost:8080"; but that didn't work.
I've generally looked for how to make the principal and claims persist through the two services.
To sum the question up. How do I authenticate a third-party app, with OpenID, through my backend, with the Identity also available to my frontend?

Related

How to retrieve AuthenticationToken while processing API request?

I've configured External provider authentication to my Blazor WASM app. User can choose to log in via Spotify account and after that, I want my server to download some data about him from Spotify API.
services.AddAuthentication()
.AddIdentityServerJwt()
.AddSpotify(options =>
{
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.CallbackPath = "/signin-spotify";
options.SaveTokens = true;
var scopes = new List<string> {
//scopes
};
options.Scope.Add(string.Join(",", scopes));
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);
ctx.Properties.IsPersistent = true;
return Task.CompletedTask;
};
});
In order to call Spotify API, I need an access token. Token is, if I understand correctly, given to my server after user logs in. In above code, I've specified OnCreatingTicket event and I can see it is being fired (just after I log in) and access_token is in tokens list.
Now, the problem is, I don't know how to retrieve that token later.
Here is what happens after log in:
User navigates to \LikedSongs (blazor wasm subpage that is meant to display data)
Blazor page calls my server's API to retrieve data that will be later displayed
protected override async Task OnInitializedAsync()
{
savedTracks = await HttpClient.GetFromJsonAsync<SavedTrack[]>("UsersTracks");
}
Finally, my API controller is being fired:
[HttpGet]
public async Task<IEnumerable<SavedTrack>> GetAsync()
{
// here I need to have access_token
// ASP.net MVC tutorial I follow states, that below line should work
var token = await _httpContextAccessor.HttpContext.GetTokenAsync("Spotify", "access_token");
// unfortunately token == null
}
For some reason, token is null. And I can't find any other tokens in HttpContext. As I understand correctly, tokens are encoded in cookies, so why I can't find any of them there?

Why does OAuth fail when using ASP.NET Identity Core?

I have an ASP.NET Core 2.x project with the following configuration:
services
.AddAuthentication(options => options.DefaultScheme = CookieAuthenticaitonDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddFacebook(ConfigureFacebook);
Predictably, when I call from one of my actions:
return Challenge(new AuthenticationProperties { RedirectUri = "/test" }, "Facebook");
... Then, I get navigated through the Facebook OAuth sequence. When I find my way back to my app, HttpContext.User.Identity is populated with the relevant details:
User.Identity.Name - The Facebook user name.
User.Identity.AuthenticationType - The string "Facebook".
User.Identity.IsAuthenticated - true.
This is all well and as is expected. However, if I add to my application configuration the following
services.AddIdentity<MyUserType, MyRoleType>()
.AddEntityFrameworkStores<MyDbContext>();
Suddenly, the OAuth flow ends in User.Identity being anonymous without anything else changing. If we drill into IdentityServiceCollectionExtensions.cs, we find:
options.DefaultAuthenticateScheme =
IdentityConstants.ApplicationScheme; options.DefaultChallengeScheme =
IdentityConstants.ApplicationScheme; options.DefaultSignInScheme =
IdentityConstants.ExternalScheme;
among other things...
What is going on here? Why is Identity interfering with the Cookie process, and what is the correct way to get the User returned from an OAuth provider?
For combining OAuth and Asp.Net Core Identity, you need to configure the facebookOptions.SignInScheme with CookieAuthenticationDefaults.AuthenticationScheme.
Try code below:
services
.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddFacebook(facebookOptions =>
{
facebookOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
facebookOptions.AppId = "xx";
facebookOptions.AppSecret = "xxx";
});
When combining ASP.NET Identity and OAuth, certain considerations need to be made:
Configuring the services:
Adding AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) is no longer necessary because Identity adds its own cookie handlers.
Getting the external user as a ClaimsPrincipal:
If you want the external user to be populated under HttpContext.User, do the following:
.AddFacebook(options => {
options.SignInScheme = IdentityConstants.ApplicationScheme;
})
After being redirected to the RedirectUri in your challenge's AuthenticationProperties, your HttpContext.User will be populated.
Getting the external user as ExternalLoginInfo:
This is preferred if you need to know things about the user such as:
From what provider did they come?
What is their unique key on the provider?
Your services should be configured like so:
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = "";
options.AppSecret = "";
});
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<MyDbContext>();
In your login controller, inject SignInManager<TUser> in:
public DefaultController(SIgnInManager<IdentityUser> signInManager)
And in your challenge action, use ConfigureExternalAuthenticationProperties to get the challenge properties:
public IActionResult LoginExternal() {
var props = SignInManager.ConfigureExternalAuthenticationProperties("Facebook", "/");
return Challenge(props, "Facebook");
}
In your return action, use GetExternalLoginInfoAsync to get the external details about the user:
public async Task<IActionResult> LoginCallback() {
var loginInfo = await SignInManager.GetExternalLoginInfoAsync();
// This object will tell you everything you need to know about the incoming user.
}

ASP.NET Core 2 - Angular & JWT Authentication

Problem: I seem unable to fetch the User or any user-related data (e.g. UserID) in any controller after the token has been recorded to browser local storage.
I've set a breakpoint and studied HttpContext member of ControllerBase instance (the client app makes request + the auth_token is kept in local storage at this stage).
You only can extract the referrer url from Headers but there's no info about tokens.
Even if you create a cookie for the token - the Request doesn't have it (0 cookies found at all).
Perhaps I misunderstand the concept of how authorization works.
Here's the bit I misunderstand most - how does ASP.NET Core fetch the token from the request made by client app - it must be kept in headers?
Also, could anyone share a working example of JWT Authentication where Angular & ASP.NET Core are separate solutions?
I've implemented login functionality and I store the access token in browser local storage.
this._authService.authenticate(this.loginModel)
.finally(finallyCallback)
.subscribe((result: AuthenticateOutput) => {
localStorage.setItem('auth_token', result.token);
});
Must the name of the token be in accordance with any conventions? (I wonder if auth_token is appropriate in this case.)
SessionController - the method which fetches current user info:
public async Task<GetCurrentLoginDetailsOutput> GetCurrentLoginDetails()
{
var output = new GetCurrentLoginDetailsOutput();
var user = await UserManager.GetUserAsync(HttpContext.User);
if (user != null)
{
output.User = Mapper.Map<UserDto>(user);
output.Tenant = Mapper.Map<TenantDto>(user.Tenant);
}
return output;
}
In my Authenticate method of AuthContoller I create Claim which holds UserID:
var user = await _userService.Authenticate(input.UserName, input.Password);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = _config.GetValidIssuer(),
Audience = _config.GetValidAudience(),
SigningCredentials = new SigningCredentials(_config.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256),
Subject = new ClaimsIdentity(new[]
{
new Claim("id", user.Id.ToString())
})
};
_userService.Authenticate method fetches the user and checks if the password is correct as follows:
var user = _context.Users.SingleOrDefault(x => x.UserName == username);
if (user == null) { return null; }
bool correctPassword = await UserManager.CheckPasswordAsync(user, password);
JWT config in Startup.cs
services
.AddAuthentication()
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = Configuration.GetSymmetricSecurityKey(),
ValidAudience = Configuration.GetValidAudience(),
ValidIssuer = Configuration.GetValidIssuer()
};
});
CORS is configured as follows:
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
Additional info:
The Angular app is a separate solution / project - not the "one solution" template available in VS2017.
I'm using ASP.NET Core v2.1
I'm using NSwag.AspNetCore package to auto-generate services for Angular project.
Here's the tutorial which I've been using to code my app.

Microsoft Authentication in ASP.NET Core 2 and Azure App Services

I have the following application at GitHub and have deployed it to https://stratml.services on an Azure App Service with Authentication defined as Microsoft Account with anymous requests requiring a Microsoft Account sign in. In "prod" this challenge occurs, however https://stratml.services/Home/IdentityName returns no content.
I have been following this and this however I do not want to use EntityFramework and from the latter's description it seems to imply if I configure my Authentication scheme correctly I do not have to.
This following code is in my Start class:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
}).AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
microsoftOptions.CallbackPath = new PathString("/.auth/login/microsoftaccount/callback");
});
Update: Thanks to the first answer I was able to get, it now authorizes to Microsoft and attempts to feedback to my application however I receive the following error:
InvalidOperationException: No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Cookies
Please visit https://stratml.services/Home/IdentityName and the GitHub has been updated.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
}).AddCookie(option =>
{
option.Cookie.Name = ".myAuth"; //optional setting
}).AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
});
I have checked this issue on my side, based on my test, you could confgure your settings as follows:
Under the ConfigureServices method, add the cookie and MSA authentication services.
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.Cookie.Name = ".myAuth"; //optional setting
})
.AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
});
Under the Configure method, add app.UseAuthentication().
TEST:
[Authorize]
public IActionResult Index()
{
return Content(this.User.Identity.Name);
}
When I checking your online website, I found that you are using the Authentication and authorization in Azure App Service and Authenticate with Microsoft account.
AFAIK, when using the app service authentication, the claims could not be attached to current user, you could retrieve the identity name via Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"] or you could follow this similar issue to manually attach all claims for current user.
In general, you could either manually enable authentication middle-ware in your application or just leverage the app service authentication provided by Azure without changing your code for enabling authentication. Moreover, you could Remote debugging web apps to troubleshoot with your application.
UPDATE:
For enable the MSA authentication in my code and test it when deployed to azure, I disabled the App Service Authentication, then deployed my application to azure web app. I opened a new incognito window and found that my web app could work as expected.
If you want to simulate the MSA login locally and use Easy Auth when deployed to azure, I assumed that you could set a setting value in appsettings.json and manually add the authentication middle-ware for dev and override the setting on azure, details you could follow here. And you could use the same application Id and configure the following redirect urls:
https://stratml.services/.auth/login/microsoftaccount/callback //for easy auth
https://localhost:44337/signin-microsoft //manually MSA authentication for dev locally
Moreover, you could follow this issue to manually attach all claims for current user. Then you could retrieve the user claims in the same way for the manually MSA authentication and Easy Auth.
If you are using App Service Authentication (EasyAuth), according to Microsoft documentation page:
App Service passes some user information to your application by using special headers. External requests prohibit these headers and will only be present if set by App Service Authentication / Authorization. Some example headers include:
X-MS-CLIENT-PRINCIPAL-NAME
X-MS-CLIENT-PRINCIPAL-ID
X-MS-TOKEN-FACEBOOK-ACCESS-TOKEN
X-MS-TOKEN-FACEBOOK-EXPIRES-ON
Code that is written in any language or framework can get the information that it needs from these headers. For ASP.NET 4.6 apps, the ClaimsPrincipal is automatically set with the appropriate values.
So basically, if you are using ASP.NET Core 2.0, you need to set the ClaimPrincipal manually. What you need to use in order to fetch this headers and set the ClaimsPrincipal is AuthenticationHandler
public class AppServiceAuthenticationOptions : AuthenticationSchemeOptions
{
public AppServiceAuthenticationOptions()
{
}
}
internal class AppServiceAuthenticationHandler : AuthenticationHandler<AppServiceAuthenticationOptions>
{
public AppServiceAuthenticationHandler(
IOptionsMonitor<AppServiceAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.FromResult(FetchAuthDetailsFromHeaders());
}
private AuthenticateResult FetchAuthDetailsFromHeaders()
{
Logger.LogInformation("starting authentication handler for app service authentication");
if (Context.User == null || Context.User.Identity == null || Context.User.Identity.IsAuthenticated == false)
{
Logger.LogDebug("identity not found, attempting to fetch from the request headers");
if (Context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
var headerId = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var headerName = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
var claims = new Claim[] {
new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", headerId),
new Claim("name", headerName)
};
Logger.LogDebug($"Populating claims with id: {headerId} | name: {headerName}");
var identity = new GenericIdentity(headerName);
identity.AddClaims(claims);
var principal = new GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal,
new AuthenticationProperties(),
Scheme.Name);
Context.User = principal;
return AuthenticateResult.Success(ticket);
}
else
{
return AuthenticateResult.Fail("Could not found the X-MS-CLIENT-PRINCIPAL-ID key in the headers");
}
}
Logger.LogInformation("identity already set, skipping middleware");
return AuthenticateResult.NoResult();
}
}
You can then write an extension method for the middleware
public static class AppServiceAuthExtensions
{
public static AuthenticationBuilder AddAppServiceAuthentication(this AuthenticationBuilder builder, Action<AppServiceAuthenticationOptions> configureOptions)
{
return builder.AddScheme<AppServiceAuthenticationOptions, AppServiceAuthenticationHandler>("AppServiceAuth", "Azure App Service EasyAuth", configureOptions);
}
}
And add app.UseAuthentication(); in the Configure() method and put following in the ConfigureServices() method of your startup class.
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "AppServiceAuth";
options.DefaultChallengeScheme = "AppServiceAuth";
})
.AddAppServiceAuthentication(o => { });
If you need full claims details, you can retrieve it on the AuthenticationHandler by making request to /.auth/me and use the same cookies that you've received on the request.

ASP.NET Core authenticating with Azure Active Directory and persisting custom Claims across requests

I have a default ASP.NET Core website created within Visual Studio 2017. I have chosen to authenticate using an Azure Active Directory.
I run the site and can successfully login using an account in the Active Directory.
I can retrieve Claim information provided by Active Directory, e.g. by calling the following line I get the 'name'.
User.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
I want to add a custom claim - CompanyId = 123456 for the logged in user.
I'm able to add a custom claim however it is only available on the page where the claim is set.
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
((ClaimsIdentity)User.Identity).AddClaim(claim);
My understanding is that I somehow need to update the token that has been issued by Active Directory or set the claim before the token is issued. I'm unsure how to do this.
I suspect this needs to be done in the AccountController at SignIn()
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
}
I've read numerous articles and samples about this scenario (including https://github.com/ahelland/AADGuide-CodeSamples/tree/master/ClaimsWebApp) however have not managed to solve how to persist the Claim across requests.
I have successfully managed to persist custom Claims using ASP.NET Identity as the Authentication Provider, but this appears to be because the custom Claim is saved to the database..
OnTokenValidated offers you the chance to modify the ClaimsIdentity obtained from the incoming token , code below is for your reference :
private Task TokenValidated(TokenValidatedContext context)
{
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
(context.Ticket.Principal.Identity as ClaimsIdentity).AddClaim(claim);
return Task.FromResult(0);
}
Setting the OpenIdConnectEvents:
Events = new OpenIdConnectEvents
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
OnTokenValidated = TokenValidated
}
Then in controller using :
var companyId= User.Claims.FirstOrDefault(c => c.Type == "CompanyId")?.Value;
For those who would like more detail, the code provided is placed in Startup.cs
In the Configure method add/edit:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
Events = new OpenIdConnectEvents
{
OnTokenValidated = TokenValidated
}
});
The private Task TokenValidated method is in the body of Startup.cs
The following sample is a good reference.
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore-v2/blob/master/WebApp-OpenIDConnect-DotNet/Startup.cs

Categories