ASP.NET Core 2 - Angular & JWT Authentication - c#

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.

Related

Authorize Google analytics data from a web application

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.

ASP.NET Core custom authentication filter for Azure AD token validation

I am currently working on a project where I need to work with Azure AD token, which is working fine. I register my application in Azure portal and use the client id in my project and this is working fine with this code (in startup.cs):
services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd");
and these settings in appsettings.json:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "35234523452345",
"ClientId": "25234523452345",
"Audience": "api://25234523452345"
},
I want to token only for one API call but with the above code it will apply on all the API. Can I use a custom filter which will use the Azure token for authentication?
Can someone help me with a code sample?
i found solution for my question, i created a method which will validate azure token, and will return me user name from user claim
public static async Task<string> ValidateAzureToken(string token, AppConfigurationList appConfigurations)
{
var tenantId = "Azure Tenant Id here";
var audience = string.Format("api://{0}", "Azure Client Id here");
var azureClientKey = "Azure Client secret Key here";
var myIssuer = string.Format(CultureInfo.InvariantCulture, "https://sts.windows.net/{0}/", tenantId);
var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(azureClientKey));
var stsDiscoveryEndpoint = string.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", tenantId);
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = await configManager.GetConfigurationAsync();
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = audience,
ValidateIssuer = true,
ValidIssuer = myIssuer,
IssuerSigningKeys = config.SigningKeys,
ValidateLifetime = false,
IssuerSigningKey = securityKey,
};
_ = (SecurityToken)new JwtSecurityToken();
SecurityToken validatedToken;
try
{
var claims = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return claims.Identity.Name.Split("#")[0];
}
catch (Exception e)
{
throw new TGUnauthorizedException("unauthorized domain user", e);
}
}
An ASP.NET Core web application that authenticates Azure AD users and calls a web API using OAuth 2.0 access tokens.
Here you can find the ASP.NET Core samples about JwtBearer. Here the Azure AD samples demonstrate WebApp calling WebApi (also bearer for the WebAPI, OIDC for the App FrontEnd.
Sample Code: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore
Reference SO Thread: Azure AD Authentication in ASP.NET Core 2.2
I think you can follow this document to set authentication for the specific api.
Based on the configuration you've done, you can manage your api controller like this
[Authorize]
public class HelloController : Controller
{
public IActionResult Index()
{
HttpContext.ValidateAppRole("Tiny.Read");
Student stu = new Student();
stu.age = 18;
return Json(stu) ;
}
}
The above verified the role based api permission, and if you prefer to use delegate api permission, you can set the api like this
[ApiController]
[Authorize]
public class HomeController : ControllerBase
{
[HttpGet]
[RequiredScope("User.Read")]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
Have you looked at the useWhen mechanism, which lets you apply a type of authentication to a particular condition - eg Path + Method:
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(new PathString("/api/mypath")) &&
ctx.Request.Method != "OPTIONS",
api => api.UseAuthentication()
);
My experimental .Net Core API does this and may give you some ideas. My objective was to write APIs in different technologies in the same way, and to take closer control over OAuth / claims processing.
In a real API it of course makes sense to keep code simple as much as you can, and of course to use proper libraries for any real security / crypto.

Still logged in MVC site, but can't call web API

I have an ASP.NET MVC site, IdentityServer4 host and a web API.
When I log in the MVC site, using external provider (Facebook), I'm logged in fine. From the MVC site I can also consume the web API correctly.
However, the next day, I'm still logged in into the MVC site, but when I then try to access the web API, I get a 'not authorized exception'.
So although I'm still logged in in the MVC site, I'm not authenticated anymore to call a web API from within the MVC site.
I'm wondering how to handle this situation, and how IdentityServer4 should be configured.
Why am I still logged in the MVC site a day later? How can this be configured?
Why can't I still call the web API, if I'm still logged in the MVC site?
Can I sync the expiration times? Or how should I handle this?
The MVC application is configured like:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = mgpIdSvrSettings.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = mgpIdSvrSettings.ClientId;
options.ClientSecret = mgpIdSvrSettings.ClientSecret; // Should match the secret at IdentityServer
options.ResponseType = "code id_token"; // Use hybrid flow
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("mgpApi");
options.Scope.Add("offline_access");
});
So it's using hybrid flow.
In IdentityServer the MVC client is configured like:
new Client
{
EnableLocalLogin = false,
ClientId = "mgpPortal",
ClientName = "MGP Portal Site",
AllowedGrantTypes = GrantTypes.Hybrid,
// where to redirect to after login
RedirectUris = mgpPortalSite.RedirectUris,
// where to redirect to after logout
PostLogoutRedirectUris = mgpPortalSite.PostLogoutRedirectUris,
// secret for authentication
ClientSecrets = mgpPortalSite.ClientSecrets.Select(cs => new Secret(cs.Sha256())).ToList(),
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"mgpApi"
},
AllowOfflineAccess = true,
RequireConsent = false,
},
And finally the web API:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = mgpIdSvrSettings.Authority;
options.RequireHttpsMetadata = false;
options.ApiName = mgpIdSvrSettings.ApiName;
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10);
});
There are two types of authentication, cookie and bearer.
Where the cookie keeps you logged in, the bearer token can't. Because the bearer token is set to expire at some point, without allowing you to change the lifetime.
The only way to access the resource (api) after the access token expires is to either let the user login again or request a new access token using a refresh token, without needing user interaction.
You've already configured it:
options.Scope.Add("offline_access");
On each login the request will at least contain a refresh token. Store it at a safe place and use it when needed. By default it is set to one time use only.
You can use something like this code to renew the token (as you are not actually refreshing it, but rather replacing it). You'll need to include the 'IdentityModel' NuGet package, as seen in the samples from IdentityServer.
private async Task<TokenResponse> RenewTokensAsync()
{
// Initialize the token endpoint:
var client = _httpClientFactory.CreateClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError) throw new Exception(disco.Error);
// Read the stored refresh token:
var rt = await HttpContext.GetTokenAsync("refresh_token");
var tokenClient = _httpClientFactory.CreateClient();
// Request a new access token:
var tokenResult = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "mvc",
ClientSecret = "secret",
RefreshToken = rt
});
if (!tokenResult.IsError)
{
var old_id_token = await HttpContext.GetTokenAsync("id_token");
var new_access_token = tokenResult.AccessToken;
var new_refresh_token = tokenResult.RefreshToken;
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
// Save the information in the cookie
var info = await HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", new_refresh_token);
info.Properties.UpdateTokenValue("access_token", new_access_token);
info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
await HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
return tokenResult;
}
return null;
}
By default the refresh token usage is configured as one time use. Please note that when storing the new refresh token fails and you should lose it, then the only way to request a new refresh token is to force the user to login again.
Also note that the refresh token can expire.
And taking it one step back, you'll need to use this when the access token expired or is about to expire:
var accessToken = await HttpContext.GetTokenAsync("access_token");
var tokenHandler = new JwtSecurityTokenHandler();
var jwtSecurityToken = tokenHandler.ReadJwtToken(accessToken);
// Depending on the lifetime of the access token.
// This is just an example. An access token may be valid
// for less than one minute.
if (jwtSecurityToken.ValidTo < DateTime.UtcNow.AddMinutes(5))
{
var responseToken = await RenewTokensAsync();
if (responseToken == null)
{
throw new Exception("Error");
}
accessToken = responseToken.AccessToken;
}
// Proceed, accessToken contains a valid token.

Use JWT with OAuth Authentication in .Net-Core

I have a custom implementation of .AddOAuth() in .Net-Core. I've created a nuget package for Authentication using Coinbase (which is basically a clone of the add google implementation plus a few custom options specific to coinbase) full source. I've looked at a few other questions on this however they don't seem to implement OAuth (e.g I cannot pass scopes) I would like to login using OAuth But I want to return to my clients a JWT.
When I try to use JWT with AddCoinbase ( which is just a derrivative of AddOAuth)
services.AddAuthentication(JWT_BEARER_AUTH)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
//TODO: get key from secret section
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
})
.AddCoinbase(options => {
options.AccessAllAccounts = true;
options.SendLimitAmount = 1;
options.SendLimitCurrency = "USD";
options.SendLimitPeriod = SendLimitPeriod.day;
options.ClientId = Configuration["Coinbase:ClientId"];
options.ClientSecret = Configuration["Coinbase:ClientSecret"];
COINBASE_SCOPES.ForEach(scope => options.Scope.Add(scope));
options.SaveTokens = true;
options.ClaimActions.MapJsonKey("urn:coinbase:avatar", "avatar_url");
});
After I login to coinbase the external callback redirects me
[HttpGet("ExternalLoginCallback")]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
//TODO: Handle remote error failure
throw new Exception($"Error from external provider: {remoteError}");
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
//TODO: Handle null external login info
throw new Exception("Error: could not find user info");
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);1
var user = await (result.Succeeded ?
_userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey)
: this.CreateIdentityUser(info));
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return Redirect(returnUrl);
}
After the redirect I never receive a JSON Web Token I always receive a Cookie. How can I leverage OAuth Authentication while serving JWT to my Clients?
OAuth is not a Json Web Token solution. OAuth 2.0 provides authorization and optionally identification (OIDC).
When you authorize via an OAuth 2.0 endpoint, you receive an Access Token and optionally an ID Token. The ID Token is a Signed JWT. The Access Token is an opaque object that is a Signed JWT for some vendor implementations but not all (Google is opaque).
After authorization you receive one or two tokens (access and ID). You can wrap them in your own JWT, sign it and then use the combined JWT any way that you want.

Azure AD federated logout not redirecting to client application

I am using Identity Server 3 for a central authentication server to a .Net MVC web application I am building.
I have configured the authentication server to use the Open ID Connect identity provider in order to allow users to authenticate against a multi-tenant Azure Active Directory account, using the Hybrid flow.
Currently, sign in works as expected with my client application redirecting to the authentication server which in turn redirects to Microsoft for login before returning back to my client application with a correctly populated Access Token.
However, when I try to logout I am redirected to Microsoft correctly, but the page stops when it arrives back at the authentication server, rather than continuing back to my client application.
I believe I have setup the post logout redirect correctly as outlined here and I think all of my settings are ok.
When I pull the Identity Server 3 code down and debug it, it is correctly setting the signOutMessageId onto the query string, but hits the following error inside the UseAutofacMiddleware method when it is trying to redirect to my mapped signoutcallback location:
Exception thrown: 'System.InvalidOperationException' in mscorlib.dll
Additional information: Headers already sent
My Authentication Server setup:
app.Map("identity", idsrvApp => {
var idSvrFactory = new IdentityServerServiceFactory();
var options = new IdentityServerOptions
{
SiteName = "Site Name",
SigningCertificate = <Certificate>,
Factory = idSvrFactory,
AuthenticationOptions = new AuthenticationOptions
{
IdentityProviders = ConfigureIdentityProviders,
EnablePostSignOutAutoRedirect = true,
PostSignOutAutoRedirectDelay = 3
}
};
idsrvApp.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
idsrvApp.UseIdentityServer(options);
idsrvApp.Map("/signoutcallback", cb => {
cb.Run(async ctx => {
var state = ctx.Request.Cookies["state"];
ctx.Response.Cookies.Append("state", ".", new Microsoft.Owin.CookieOptions { Expires = DateTime.UtcNow.AddYears(-1) });
await ctx.Environment.RenderLoggedOutViewAsync(state);
});
});
});
My Open Id Connect setup to connect to Azure AD:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "aad",
SignInAsAuthenticationType = signInAsType,
Authority = "https://login.microsoftonline.com/common/",
ClientId = <Client ID>,
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = Constants.ExternalAuthenticationType,
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(<Client ID>, <Client Secret>);
string tenantId = context.AuthenticationTicket.Identity.FindFirst("tid").Value;
AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{tenantId}");
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(<Identity Server URI>/aad/"), credential, "https://graph.windows.net");
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/aad/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl + "/signoutcallback";
if (context.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnectRequestType.LogoutRequest)
{
var signOutMessageId = context.OwinContext.Environment.GetSignOutMessageId();
if (signOutMessageId != null)
{
context.OwinContext.Response.Cookies.Append("state", signOutMessageId);
}
}
return Task.FromResult(0);
}
});
I cannot find any information about the cause of or solution to this problem. How do I configure this to correctly redirect back to my client application?
Edit:
Related discussion on GitHub: https://github.com/IdentityServer/IdentityServer3/issues/2657
I have also tried this with the latest version of Identity Server on MyGet (v2.4.1-build00452) with the same problem.
I have also created a repository that reproduces the issue for me here: https://github.com/Steve887/IdentityServer-Azure/
My Azure AD setup:
I believe you were experiencing a bug that is fixed in 2.5 (not yet released as of today): https://github.com/IdentityServer/IdentityServer3/issues/2678
Using current source from Git, I still see this problem. It appears to me that AuthenticationController.Logout is hit twice during the logout. Once prior to the external provider's logout page is displayed, and once after. The initial call Queues and clears the signout cookie so that the second time it is not available when rendering the logout page.

Categories