Authorize Google analytics data from a web application - c#

I am trying to authorize an ASP.NET Core 6 MVC web app to Google analytics data API.
[GoogleScopedAuthorize("https://www.googleapis.com/auth/analytics.readonly")]
public async Task<IActionResult> Index([FromServices] IGoogleAuthProvider auth)
{
var cred = await auth.GetCredentialAsync();
var client = await BetaAnalyticsDataClient.CreateAsync(CancellationToken.None);
var request = new RunReportRequest
{
Property = "properties/" + XXXXX,
Dimensions = {new Dimension {Name = "date"},},
Metrics = {new Metric {Name = "totalUsers"},new Metric {Name = "newUsers"}},
DateRanges = {new DateRange {StartDate = "2021-04-01", EndDate = "today"},},
};
var response = await client.RunReportAsync(request);
}
The authorization goes though as would be expected; I am getting an access token back.
I cant seem to figure out how to apply the credentials to the BetaAnalyticsDataClient.
When I run it without applying it to the BetaAnalyticsDataClient, I get the following error:
InvalidOperationException: The Application Default Credentials are not available. They are available if running in Google Compute Engine. Otherwise, the environment variable GOOGLE_APPLICATION_CREDENTIALS must be defined pointing to a file defining the credentials. See https://developers.google.com/accounts/docs/application-default-credentials for more information.
I am not currently using GOOGLE_APPLICATION_CREDENTIALS as it is configured in programs.cs. I don't see the need to have client id and secret configured in program.cs plus having an added env var.
Why isn't it just picking up the authorization already supplied with the controller runs?
builder.Services
.AddAuthentication(o =>
{
// This forces challenge results to be handled by Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This forces forbid results to be handled by Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// Default scheme that will handle everything else.
// Once a user is authenticated, the OAuth2 token info is stored in cookies.
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
options.ClientId = builder.Configuration["Google:ClientId"];
options.ClientSecret = builder.Configuration["Google:ClientSecret"];
});
Is there an alternate method for authorizing with a web app that I have not been able to find. I did do some dinging in the source code I can't seem to find a method to apply this.

After quite a bit of digging i managed to find that it was possible to create my own client builder and apply the credentials there.
var clientBuilder = new BetaAnalyticsDataClientBuilder()
{
Credential = await auth.GetCredentialAsync()
};
var client = await clientBuilder.BuildAsync();
Hope this helps someone else.

Related

I just want to access some files from my .net core web application using Microsoft Graph and nothing is current

I have a .net core web application and I just want to access some files that are on one of our SharePoint sites, using Microsoft Graph. I've looked at courses on Pluralsight and the most current course has outdated material. I'm looking for a simple code example that gets me from a - z and I can't find any information that exists before mid 2022! On a similar question, I got an answer with code that didn't even work. Apparently I have to get an authorization code, in order to get an access token. BUT, the authorization code pretty much expires as soon as the user is logged into my application. Below is a modified version of the code I was given. I modified it in an effort to try to make it work. As you will see, I tried various version of "scopes" and I'm getting a token that I'm trying to use in the AuthorizationCode Credentials. I don't know if it's the right token to use. I've also seen some examples using PostMan. Getting things to work in postman is absolutely wonderful, but it's not C# code. I apologize if I seem a little rough, I'm just extremally frustrated. It should not be this difficult to find a working code sample. Any help would be appreciated. Here is the code I have that doesn't work:
//var scopes = new[] { "https://mysite.sharepoint.com/.default" };
//var scopes = new[] { "https://graph.microsoft.com/.default" };
var scopes = new[] { "https://graph.microsoft.com/User.ReadWrite.All" };
var tenantId = "tenant";
var clientId = "clientId";
var clientSecret = "shhItsASecret";
var client = new RestClient("https://login.microsoftonline.com/siteId/oauth2/v2.0/token");
var request = new RestRequest();
request.Method = Method.Post;
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("client_id", clientId);
request.AddParameter("client_secret", clientSecret);
request.AddParameter("scope", "https://graph.microsoft.com/User.ReadWrite.All");
request.AddParameter("response_type", "code");
request.AddParameter("grant_type", "client_credentials");
RestResponse response = client.Execute(request);
TokenModel tokenModel = new TokenModel();
JsonConvert.PopulateObject(response.Content, tokenModel);
var authorizationCode = tokenModel.access_token;
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.authorizationcodecredential
var authCodeCredential = new AuthorizationCodeCredential(tenantId, clientId, clientSecret, authorizationCode, options);
Azure.Core.AccessToken accessToken = new Azure.Core.AccessToken();
try
{
accessToken = await authCodeCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(scopes) { });
}
catch (System.Exception ex)
{
throw;
}
var tok = accessToken;
UPDATE:
I now know that I need to use delegated permissions and I need to use the auth code flow in order to do that. However, we use 2 factor authentication and it seems that by the time I can read anything from a variable, I can only see an access-token. If I understand correctly, the auth code is used to get an access-token and it expires. So, I can't seem to use that. Could I pass that access-token to my code that instantiates the graphService?
Someone else suggested I need to adjust my startup file and my appsettings file. I can't really do that. We have 5 other modules in our web application and this would be a big change to all of that. So, I'm not sure what I should be doing there. Bellow is what is in our startup, as it pertains to authentication:
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<UnitRateContractSystemContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<ApplicationUser, ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserClaim<Guid>, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>>()
.AddRoleStore<RoleStore<ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserRole, IdentityRoleClaim<Guid>>>();
UPDATE 3:
I looked a little further down in my startup file and there was some openID connect information. Not sure why it was moved so far down, but I moved it up. Below is my entire authentication setup. The last 4 lines I added as a result of following one of the examples that someone provided. It builds just fine, but when I run it, I get an error in the Program.cs file: System.InvalidOperationException: 'Scheme already exists: Cookies'. If I go and comment out the "AddCookie()" line I get a similar error, but it says that OpenId Connect exists. So, at this point I'm stuck, but I feel if this can be solved, it might be the solution.
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<UnitRateContractSystemContext>()
.AddDefaultTokenProviders()
.AddUserStore<UserStore<ApplicationUser, ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserClaim<Guid>, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>>()
.AddRoleStore<RoleStore<ApplicationRole, UnitRateContractSystemContext, Guid, ApplicationUserRole, IdentityRoleClaim<Guid>>>();
#region Authentication
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
//auth
services.AddAuthentication(options =>
{
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["Authentication:Microsoft:OAuth"];
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.UsePkce = false;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("email");
options.SaveTokens = true;
options.CallbackPath = new PathString(Configuration["Authentication:Microsoft:Callback"]);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
};
// MetadataAddress represents the Active Directory instance used to authenticate users.
options.MetadataAddress = Configuration["Authentication:Microsoft:Meta"];
options.ClientId = Configuration["Authentication:Microsoft:ApplicationId"];
options.ClientSecret = Configuration["Authentication:Microsoft:Password"];
})
.AddMicrosoftIdentityWebApp(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
You have an asp.net core web application, and you want to access some files that are on one of your SharePoint sites. So I think you may want to use this graph api with request /sites/{site-id}/drive/items/{item-id}. If you want to use other APIs, the steps are the same.
First, since the scenario for you is access files in different sites, so if you used delegated permission(require users sign in first and get access token on behalf the user), you may meet an issue that the user is not allowed to this site so that he can't access the site. I'm afraid this is what you want, so you can use application permissions. For this api, the permission is like below, please add api permissions in Azure AD first.
Then, since you have an asp.net core web application, then you can use Azure identity + graph SDK to do this. You can use code below:
using Microsoft.Graph;
using Azure.Identity;
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_name.onmicrosoft.com";
var clientId = "aad_app_id";
var clientSecret = "client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var file = await graphClient.Sites["site_id"].Drive.Items["item_id"].Request().GetAsync();
If you want to let user sign in and then list some files which is allowed to the signed in user, the easiest way is adding Microsoft identity platform into your application, which can created by a template, just need to choose the authentication option when creating application in visual studio, then update the configurations. And certainly, you need to give delegated API permission, which is different than above.
Finally here's the official sample, you can see what codes/packages/configurations are added based on a web application.

MSAL Issues when integrating Azure AD Authentication and Authorization

I have enabled authentication for azure ad by following some sample apps from MS as shown below. I then use the authorize statements below as well so my application should be locked down unless the user is authenticated. My issue in my testing environment is if i restart IIS express then when it starts back up I am not required to log back in if the browser is still open. I can openly navigate my application but when any function related to MS Graph is called it fails with error: ErrorCode: user_null Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. So its as if my application thinks I am still authorized but really I am not because the MS Graph api call fails because of token related issues. If I force myself to logout then log back in everything works. For my live site if a user is inactive for some time but the browser is left open this same issue occurs and they are not forced to relogin so these issues again can happen until they force logout and re log back in. What have I setup wrong or need to add to force relogin before these issues arise? Or can I keep the token working without forcing the user to relogin?
// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
});
//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
//Tap into this event to add a UserID Claim to a new HttpContext identity
OnTokenValidated = context =>
{
//This query returns the UserID from the DB by sending the email address in the claim from Azure AD
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
})
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
Then in all of my controllers I use:
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
Adding
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(initialScopes);
before my graph api call fixes my issue. However, I feel as though I shouldnt need to call this everytime but only if the token expires. So is there a way to check if the token expires?
There are two things that worked for me. One was trying to get a token manually if an error occurred using the following:
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(initialScopes);
The other solution that seemed to work the same was to add the following catch as well:
try
{
Console.WriteLine($"{svcex}");
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
_consentHandler.ChallengeUser(initialScopes, claimChallenge);
}
catch (Exception ex2)
{
_consentHandler.HandleException(ex2);
}
I do not know enough about what consent handler does to provide more info on why this works but will update my answer once I do.

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?

IdentityServer4 as external provider, how to avoid logout prompt?

I am working with two identity providers, both implemented using IdentityServer4 in ASP.NET MVC Core 2.2. One of them is used as an external provider by the other. Let's call them "primary" and "external". The primary provider is referenced directly by the web application. The external provider is an optional login method provided by the primary provider.
The web application uses the oidc-client-js library to implement authentication. The logout operation in the web app calls UserManager.signoutRedirect. This works fine when the primary identity provider is used (no logout confirmation prompt is shown). However, when the external provider is used, the user is prompted to sign out from the external provider.
The sequence of requests when logging out are:
GET http://{primary}/connect/endsession?id_token_hint=...&post_logout_redirect_uri=http://{webapp}
GET http://{primary}/Account/Logout?logoutId=...
GET http://{external}/connect/endsession?state=...&post_logout_redirect_uri=http://{primary}/signout-callback-{idp}&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
GET http://{external}/Account/Logout?logoutId=...
This last request above shows the logout confirmation screen from the external provider.
The code for the /Account/Logout page on the primary provider is almost identical to the sample code in the documentation:
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
var vm = await BuildLogoutViewModelAsync(logoutId);
if (!vm.ShowLogoutPrompt)
{
// If the request is authenticated don't show the prompt,
// just log the user out by calling the POST handler directly.
return Logout(vm);
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated)
{
// delete local authentication cookie
await _signInManager.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
var ap = new AuthenticationProperties { RedirectUri = url };
return SignOut(ap, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
The BuildLogoutViewModelAsync method calls GetLogoutContextAsync to check if the logout is authenticated, like so:
public async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel
{
LogoutId = logoutId,
ShowLogoutPrompt = true
};
var context = await _interaction.GetLogoutContextAsync(logoutId);
if (context?.ShowSignoutPrompt == false)
{
// It's safe to automatically sign-out
vm.ShowLogoutPrompt = false;
}
return vm;
}
The BuildLoggedOutViewModelAsync method basically just checks for an external identity provider and sets the TriggerExternalSignout property if one was used.
I hate to make this a wall of code, but I'll include the ConfigureServices code used to configure the primary identity server because it is probably relevant:
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme;
opts.Authority = openIdConfig.ProviderAuthority;
opts.ClientId = openIdConfig.ClientId;
opts.ClientSecret = openIdConfig.ClientSecret;
opts.ResponseType = "code id_token";
opts.RequireHttpsMetadata = false;
opts.CallbackPath = $"/signin-{openIdConfig.Scheme}";
opts.SignedOutCallbackPath = $"/signout-callback-{openIdConfig.Scheme}";
opts.RemoteSignOutPath = $"/signout-{openIdConfig.Scheme}";
opts.Scope.Clear();
opts.Scope.Add("openid");
opts.Scope.Add("profile");
opts.Scope.Add("email");
opts.Scope.Add("phone");
opts.Scope.Add("roles");
opts.SaveTokens = true;
opts.GetClaimsFromUserInfoEndpoint = true;
var mapAdditionalClaims = new[] { JwtClaimTypes.Role, ... };
foreach (string additionalClaim in mapAdditionalClaims)
{
opts.ClaimActions.MapJsonKey(additionalClaim, additionalClaim);
}
opts.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
}
My understanding is that the id_token_hint parameter passed to the first /connect/endsession endpoint will "authenticate" the logout request, which allows us to bypass the prompt based on the ShowSignoutPrompt property returned by GetLogoutContextAsync. However, this does not happen when the user is redirected to the external provider. The call to SignOut generates the second /connect/endsession URL with a state parameter, but no id_token_hint.
The logout code in the external provider is basically the same as the code shown above. When it calls GetLogoutContextAsync, that method does not see the request as authenticated, so the ShowSignoutPrompt property is true.
Any idea how to authenticate the request to the external provider?
The final block of code, you hate, but luckily added, contains one significant row:
opts.SaveTokens = true;
That allows you later to restore the id_token you got from the external provider.Then you can use it as a "second level hint".
if (vm.TriggerExternalSignout)
{
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
var props = new AuthenticationProperties {RedirectUri = url};
props.SetParameter("id_token_hint", HttpContext.GetTokenAsync("id_token"));
return SignOut(props, vm.ExternalAuthenticationScheme);
}
I had the exact same problem as OP and was able to correct it by explicitly stating that the ID Token is to be added on to the logout request as per this Github Issue
https://github.com/IdentityServer/IdentityServer4/issues/3510
options.SaveTokens = true; // required for single sign out
options.Events = new OpenIdConnectEvents // required for single sign out
{
OnRedirectToIdentityProviderForSignOut = async (context) => context.ProtocolMessage.IdTokenHint = await context.HttpContext.GetTokenAsync("id_token")
};
I have come up with a solution, though it seems to contradict what is done in the samples.
The problem seems to be caused by two lines of code that were both from the IdentityServer samples that we used as a basis for our IDP implementations. The problem code is in the "primary" IDP.
The first line is in ConfigureServices in Startup.cs:
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme; // this is a problem
The second place is in ExternalController.cs, in the Callback method. Here we diverged from the samples, using IdentityServerConstants.ExternalCookieAuthenticationScheme instead of IdentityConstants.ExternalScheme:
// Read external identity from the temporary cookie
var result = await this.HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// ...
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme); // this is a problem
What happens at logout is: since the SignOutScheme is overridden, it is looking for a cookie that doesn't exist. Simply removing that doesn't fix it because the call to SignOutAsync has deleted the cookie that contains the information required for the identity code to authenticate the scheme. Since it can't authenticate the scheme, it does not include the id_token_hint in the request to the "external" IDP.
I've been able to fix this by removing the code that overrides SignOutScheme in Startup.cs, and moving the code that deletes the ExternalCookieAuthenticationScheme cookie to the Logout endpoint in AccountController.cs:
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// delete temporary cookie used during external authentication
await this.HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// build a return URL so the upstream provider will redirect back...
This way the "temporary" external cookie is left around until it is needed, but is deleted when the user logs out.
I'm not sure if this is the "correct" solution, but it does seem to work correctly in all cases that I've tested. I'm not really sure why we deviated from the sample in ExternalController.cs, either, but I suspect it is because we have two standalone IDP rather than a site with a single standalone IDP. Also, the sample appears to be using implicit flow while we are using hybrid flow.

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.

Categories