Getting 401 from custom Api when using Microsoft.Identity.platform to protect api - asp.net-web-api2

I am following the tutorial from Microsfot.document for how to protect api using Azure AD (Microsoft Identity).
The steps I took are following: Sorry I tried to put information that might be helpful but too much to get to the issue most of the time contributors ask for screenshot or the code.
I followed several documents and video tutorials but here is the link for one of them: https://learn.microsoft.com/en-us/learn/modules/identity-secure-custom-api/2-secure-api-microsoft-identity
WebApi.
Created a webapi using core 5. Register it in Azure AD.
Created single scope 'check' and allowed permission to user and admin.
Webapp
Created webapp using .net(classic) Note that webapi is core 5.
Created a webapp and register it in Azure AD.
Setup the authentication and created a OnAuthorizationCodeReceived to get the access token to call the api.
Configuration:
1.From Azure AD->Registration for Webapi-> selected application(web app created above) and give permission to the scope.
2. For Azure AD->Registration for webapp-> Access permission->delegate->selected the scope.
Test:
1.Run the test. At this point; I do not have [Authorization] on the api method which I am calling.
2. Webapp successfully able to get the string which is returned by the api. Somewhat I get the idea that plumbing was right.
Added [Authorize] filter on the app method.
Result 401 unauthorized.
I have checked multiple times and looked at multiple tutorial and rewrote my code, watched several videos and updated my code but I am always getting 401 error.
Below is the code;
Api controller:
namespace Utility.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class AzAdUtility : ControllerBase
{
// GET: api/<AzAdUtility>
[HttpGet]
public string Get()
{
//HttpContext.VerifyUserHasAnyAcceptedScope(new string[] {"check"});
var name = "Vaqas";
return name;
}
}
}
Api startup :
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "GlobalNetApiUtility", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Utility v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Api Appsettings:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "myorg.onmicrosoft.com",
"ClientId": "abcd.............................",
"TenantId": "dabcd.............................."
},
Webapp startup:
Only adding startup page because at first all I am doing getting some data for testing purpose in the OnAuthorizationCodeReceived.
public class Startup
{
// The Client ID is used by the application to uniquely identify itself to Azure AD.
static string clientId = System.Configuration.ConfigurationManager.AppSettings["ClientId"];
// RedirectUri is the URL where the user will be redirected to after they sign in.
string redirectUri = System.Configuration.ConfigurationManager.AppSettings["RedirectUri"];
// Tenant is the tenant ID (e.g. contoso.onmicrosoft.com, or 'common' for multi-tenant)
static string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
// Authority is the URL for authority, composed by Microsoft identity platform endpoint and the tenant name (e.g. https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0)
string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
//string authority = "https://login.microsoftonline.com/" + tenant + "/adminconsent?client_id=" + clientId;
string clientSecret = System.Configuration.ConfigurationManager.AppSettings["ClientSecret"];
/// <summary>
/// Configure OWIN to use OpenIdConnect
/// </summary>
/// <param name="app"></param>
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
//ResponseType = OpenIdConnectResponseType.CodeIdToken,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed
}
}
);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
notification.HandleCodeRedemption();
var idClient = ConfidentialClientApplicationBuilder.Create(clientId)
.WithRedirectUri(redirectUri)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
try
{
var apiScope = "api://28......................../check2 api://28................/check api://28...........................1d/AzAdUtility.Get";
string[] scopes = apiScope.Split(' ');
//gettig the token no issues.
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
var myurl = "https://localhost:99356/api/AzAdUtility";
var client = new HttpClient();
// var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(Constants.ProductCatalogAPI.SCOPES);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); //accessToken
var json = await client.GetStringAsync(myurl);
var serializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
//getting 401 response
var checkResponse = JsonSerializer.Deserialize(json, typeof(string), serializerOptions) as string;
}
catch (Exception ex)
{
string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
}
}
/// <summary>
/// Handle failed authentication requests by redirecting the user to the home page with an error in the query string
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("Error/AccessDenied/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
}

In Api startup class I was missing app.UseAuthentication().
I never really thought that would be an issue. Once I added this code. I got the expected response rather than 401 Unauthorized error

Related

ServiceStack ServiceClient stores wrong cookies after authentication

i have a strange problem with Servicestack Authentication.
I've developed an Asp .Net Core web app (.net core 3.1) in which is implemented a servicestack authentication with credentials auth provider. Everything work correctly if i authenticate with any browsers.
Instead if i try to authenticate from external application with JsonServiceClient pointing to servicestack /auth/{provider} api i've this problem:
authentication goes well but the JsonServiceClient object stores a SessionId in cookies (s-id/s-pid) different from the SessionId of AuthenticateResponse. Here my example.
Authenticate request = new Authenticate()
{
provider = "credentials",
UserName = username,
Password = password,
RememberMe = true
};
var client = new JsonServiceClient(webappUrl);
AuthenticateResponse response = await client.PostAsync(request);
var cookies = client.GetCookieValues();
If i check values in cookies variable i see that there are s-id and s-pid completely different from the sessionId of the response.
The other strange thing is that if i repeat the authentication a second time under those lines of code, now the s-pid cookie is equal to sessionId of response!
Why??
In the startup of web app i have these lines of code:
public new void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => options.EnableEndpointRouting = false);
// Per accedere all'httpcontext della request
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Per accedere alla request context della request
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
// Registro il json di configurazione (innietta l'appSettings)
services.AddSingleton(Configuration);
// Filters
services.AddSingleton<ModulePermissionFilter>();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
... other lines of code
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IBackgroundJobClient backgroundJobs)
{
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseServiceStack(new AppHost
{
AppSettings = new NetCoreAppSettings(Configuration)
});
}
public class AppHost : AppHostBase
{
public AppHost() : base("webapp", typeof(BaseServices).Assembly) { }
// Configure your AppHost with the necessary configuration and dependencies your App needs
public override void Configure(Container container)
{
SetConfig(new HostConfig
{
UseCamelCase = false,
WriteErrorsToResponse = true,
ReturnsInnerException = true,
AllowNonHttpOnlyCookies = false,
DebugMode = AppSettings.Get(nameof(HostConfig.DebugMode), HostingEnvironment.IsDevelopment()),
// Restrict cookies to domain level in order to support PflowV2
RestrictAllCookiesToDomain = !string.IsNullOrEmpty(AppSettings.Get("RestrictAllCookiesToDomain", "")) && AppSettings.Get("RestrictAllCookiesToDomain", "").ToLower() != "localhost" ? AppSettings.Get("RestrictAllCookiesToDomain", "") : null
});
// Create DBFactory for cache
var defaultConnection = appHost.AppSettings.Get<string>("ConnectionStrings:Webapp");
var dbFactory = new OrmLiteConnectionFactory(defaultConnection, SqlServerDialect.Provider);
// Register ormlite sql session and cache
appHost.Register<IDbConnectionFactory>(dbFactory);
appHost.RegisterAs<OrmLiteCacheClient, ICacheClient>();
appHost.Resolve<ICacheClient>().InitSchema();
appHost.Register<ISessionFactory>(new SessionFactory(appHost.Resolve<ICacheClient>()));
//Tell ServiceStack you want to persist User Auth Info in SQL Server
appHost.Register<IAuthRepository>(new OrmLiteAuthRepository(dbFactory));
appHost.Resolve<IAuthRepository>().InitSchema();
var sessionMinute = appHost.AppSettings.Get("SessionTimeoutMinute", 15);
// Adding custom usersession and custom auth provider
Plugins.Add(new AuthFeature(() => new CustomUserSession(), new IAuthProvider[] { new CustomCredentialsAuthProvider(), new ApiKeyAuthProvider() })
{
HtmlRedirect = "/Account/Login", // Redirect to login if session is expired
IncludeAssignRoleServices = false,
SessionExpiry = TimeSpan.FromHours(sessionMinute),
});
Plugins.Add(new SessionFeature());
}
}

How to set up different authentication for API and UI

I am using ASP.NetCore 2.2 (probably moving to 3.0 soon). I have an Azure App Service application.
I want to have an API that clients will use an API token (client secret) to authenticate with so that they can run without requiring interactive authorization.
The UI portion will require Azure Active Directory authentication.
How do I wire this up these two different auth methods my ASP.Net Core app?
How-to
Firstly, we need an AuthenticationHandler & Options to authenticate request with an API Token(Client Secret). Suppose you've created such an Authentication Handler & Options:
public class ClientSecretAuthenOpts : AuthenticationSchemeOptions
{
public const string DefaultAuthenticationSchemeName = "ClientSecret";
}
public class ClientSecretAuthenticationHandler : AuthenticationHandler<ClientSecretAuthenOpts>
{
public ClientSecretAuthenticationHandler(IOptionsMonitor<ClientSecretAuthenOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// ... authenticate request
}
}
And then register multiple Authentication Schemes one by one:
// add mulitple authentication schemes
services.AddAuthentication(AzureADDefaults.AuthenticationScheme) // set AzureAD as the default for users (using the UI)
.AddAzureAD(options => Configuration.Bind("AzureAD", options)) // setup AzureAD Authentication
.AddScheme<ClientSecretAuthenOpts,ClientSecretAuthenticationHandler>( // setup ClientSecret Authentication
ClientSecretAuthenOpts.DefaultAuthenticationSchemeName,
opts=>{ }
);
// post configuration for OIDC
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>{
options.Authority = options.Authority + "/v2.0/"; // Microsoft identity platform
options.TokenValidationParameters.ValidateIssuer = false; // accept several tenants
});
Finally, in order to enable multiple authentication schemes at the same time, we need override the default policy:
services.AddAuthorization(opts => {
// allow AzureAD & our own ClientSecret Authentication at the same time
var pb = new AuthorizationPolicyBuilder(
ClientSecretAuthenOpts.DefaultAuthenticationSchemeName,
"AzureAD"
);
opts.DefaultPolicy = pb.RequireAuthenticatedUser().Build();
});
Demo & Test
Suppose your API token (client secret) is sent in Request Header as below :
GET https://localhost:5001/Home/Privacy HTTP/1.1
Api-Subscription-Id: Smith
Api-Subscription-Key: top secret
To avoid hardcoding the header name, I create add two properties in the options:
public class ClientSecretAuthenOpts : AuthenticationSchemeOptions
{
public const string DefaultAuthenticationSchemeName = "ClientSecret";
public string ApiClientIdHeadername {get;set;}= "Api-Subscription-Id";
public string ApiClientTokenHeaderName {get;set;}= "Api-Subscription-Key";
}
In order to authenticate above request, I create a custom Authentication Handler as below:
public class ClientSecretAuthenticationHandler : AuthenticationHandler<ClientSecretAuthenOpts>
{
public ClientSecretAuthenticationHandler(IOptionsMonitor<ClientSecretAuthenOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// if there's no header for Client ID & Client Sercet, skip
if(
Context.Request.Headers.TryGetValue(Options.ApiClientIdHeadername, out var clientIdHeader) &&
Context.Request.Headers.TryGetValue(Options.ApiClientTokenHeaderName, out var clientSecretHeader)
){
// validate client's id & secret
var clientId = clientIdHeader.FirstOrDefault();
var clientKey = clientSecretHeader.FirstOrDefault();
var (valid, id) = await ValidateApiKeyAsync(clientId, clientKey);
if(!valid){
return AuthenticateResult.Fail($"invalid token:{clientKey}");
}else{
var principal = new ClaimsPrincipal(id);
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), this.Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
return AuthenticateResult.NoResult();
}
private Task<(bool, ClaimsIdentity)> ValidateApiKeyAsync(string clientId,string clientSecret)
{
ClaimsIdentity id = null;
// fake: need check key against the Database or other service
if(clientId=="Smith" && clientSecret == "top secret"){
id = new ClaimsIdentity(
new Claim[]{
new Claim(ClaimTypes.NameIdentifier, "client id from db or from the request"),
new Claim("Add Any Claim", "add the value as you like"),
// ...
}
,this.Scheme.Name
);
return Task.FromResult((true, id));
}
return Task.FromResult((false,id));
}
}
Test
Let's say we have a controller Action annotated with the [Authorize] attribute
[Authorize]
public IActionResult Privacy()
{
return Ok("hello,world");
}
When accessing the url within a browser (UI, without the header), the user will be redirected to Azure AD Authentication if he's not signed in.
When testing the above request with a client secret,
And we'll get the "hello,world" response:

Version problem using MSAL to access Azure AD and get back an auth id token

Asp.Net Core v2.2.0
Microsoft.AspNetCore.Authentication.AzureAD.UI v2.2.0
Microsoft.Identity.Client v4.2.1
I'm receiving the following error when logging into Azure AD and then requesting an auth id token:
While searching for a solution, the closest thing I've found is that there's an issue with using two different versions of the auth api. V2 uses login.microsoftonline.com and V1 uses sts.windows.net. The question I have is how to get everything in the MSAL library to use V2.
Here's my Startup class. It's based (largely copied) from the doc: Web app that calls web APIs - code configuration
public class Startup
{
private const string AzureAdConfigSectionName = "AzureAd";
private ConfidentialClientApplicationOptions applicationOptions;
private AzureADOptions azureAdOptions;
private MsalPerUserSessionTokenCacheProvider userTokenCacheProvider;
private MsalAppSessionTokenCacheProvider appTokenCacheProvider;
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
applicationOptions = new ConfidentialClientApplicationOptions();
Configuration.Bind(AzureAdConfigSectionName, applicationOptions);
azureAdOptions = new AzureADOptions();
Configuration.Bind(AzureAdConfigSectionName, azureAdOptions);
//services.AddOptions<AzureADOptions>();
var adOptionsMonitor = services.BuildServiceProvider().GetService<IOptionsMonitor<AzureADOptions>>();
userTokenCacheProvider = new MsalPerUserSessionTokenCacheProvider();
appTokenCacheProvider = new MsalAppSessionTokenCacheProvider(adOptionsMonitor);
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind(AzureAdConfigSectionName, options));
ConfigureSession(services);
ConfigureTokenHandling(services);
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
private void ConfigureSession(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
private void ConfigureTokenHandling(IServiceCollection services)
{
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
// Response type. We ask ASP.NET to request an Auth Code, and an IDToken
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
// This "offline_access" scope is needed to get a refresh token when users sign in with
// their Microsoft personal accounts
// (it's required by MSAL.NET and automatically provided by Azure AD when users
// sign in with work or school accounts, but not with their Microsoft personal accounts)
options.Scope.Add("offline_access");
options.Scope.Add("user.read"); // for instance
// Handling the auth redemption by MSAL.NET so that a token is available in the token cache
// where it will be usable from Controllers later (through the TokenAcquisition service)
var handler = options.Events.OnAuthorizationCodeReceived;
options.Events.OnAuthorizationCodeReceived = async context =>
{
// As AcquireTokenByAuthorizationCode is asynchronous we want to tell ASP.NET core
// that we are handing the code even if it's not done yet, so that it does
// not concurrently call the Token endpoint.
context.HandleCodeRedemption();
// Call MSAL.NET AcquireTokenByAuthorizationCode
var application = BuildConfidentialClientApplication(context.HttpContext,
context.Principal);
var scopes = new [] { "user.read" };
var scopesRequestedByMsalNet = new[] { "openid", "profile", "offline_access" };
var result = await application
.AcquireTokenByAuthorizationCode(scopes.Except(scopesRequestedByMsalNet),
context.ProtocolMessage.Code)
.ExecuteAsync();
// Do not share the access token with ASP.NET Core otherwise ASP.NET will cache it
// and will not send the OAuth 2.0 request in case a further call to
// AcquireTokenByAuthorizationCodeAsync in the future for incremental consent
// (getting a code requesting more scopes)
// Share the ID Token so that the identity of the user is known in the application (in
// HttpContext.User)
context.HandleCodeRedemption(null, result.IdToken);
// Call the previous handler if any
await handler(context);
};
});
}
/// <summary>
/// Creates an MSAL Confidential client application
/// </summary>
/// <param name="httpContext">HttpContext associated with the OIDC response</param>
/// <param name="claimsPrincipal">Identity for the signed-in user</param>
/// <returns></returns>
private IConfidentialClientApplication BuildConfidentialClientApplication(HttpContext httpContext,
ClaimsPrincipal claimsPrincipal)
{
var request = httpContext.Request;
// Find the URI of the application)
var currentUri = UriHelper.BuildAbsolute(request.Scheme,
request.Host,
request.PathBase,
azureAdOptions.CallbackPath ?? String.Empty);
// Updates the authority from the instance (including national clouds) and the tenant
var authority = $"{azureAdOptions.Instance}{azureAdOptions.TenantId}/";
// Instantiates the application based on the application options (including the client secret)
var app = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(applicationOptions)
.WithRedirectUri(currentUri)
.WithAuthority(authority)
.Build();
// Initialize token cache providers. In the case of Web applications, there must be one
// token cache per user (here the key of the token cache is in the claimsPrincipal which
// contains the identity of the signed-in user)
userTokenCacheProvider?.Initialize(app.UserTokenCache, httpContext, claimsPrincipal);
appTokenCacheProvider?.Initialize(app.AppTokenCache, httpContext);
return app;
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios,
// see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession();
app.UseAuthentication();
app.UseMvc();
}
}
The context received by the OnAuthorizationCodeReceived event, has the following:
JwtSecurityToken.Issuer = https://sts.windows.net
Not sure why, but that's where the issue is coming from.
appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "{domain}",
"TenantId": "{tenant id}",
"ClientId": "{client id}",
"CallbackPath": "/signin-oidc",
"ClientSecret": "{client secret}"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
The problem turned out to be I was using
AzureADDefaults.OpenIdScheme
instead of
AzureADDefaults.AuthenticationScheme (Default Azure AD scheme)
Which makes perfect sense, considering the problem.

IdentityServer4 losing original returnUrl when using External Login server

Unfortunately, given the size of the project, I can’t easily share a reproducible version. However, hopefully what I have below will shed some light on my issue and you’ll see where I made a mistake.
I have two sites, an ASP.Net Core MVC application and a Login server, also ASP.Net Core MVC. Let’s call them http://mvc.mysite.com and http://login.mysite.com. Neither are significantly different from the IdentityServer4 Quickstart #6. The only real difference is that I have implemented an external login provider for AzureAd. My code for that is below.
Scenario 1
Given an internal login flow, where the user uses an internal login page at http://login.mysite.com everything works fine.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login
User logs in with correct username/password
User is redirected to http://mvc.mysite.com/clients/client-page-1
Scenario 2
However, if the login server’s AccountController::Login() method determines there is a single ExternalLoginProvider and executes the line “return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);” then the original redirectUrl is lost.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login (receiving the output of AccountController::ExternalLogin)
User is redirected to AzureAd External OIDC Provider
User logs in with correct username/password
User is redirected to http://login.mysite.com/Account/ExternalLoginCallback
User is redirected to http://mvc.mysite.com (Notice that the user is redirected to the root of the MVC site instead of /clients/client-page-1)
For Scenario 1:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com/clients/client-page-1”
For Scenario 2:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com” (missing the /client.client-page-1)
In my login server’s Startup.cs I have added this to ConfigureServices:
services.AddAuthentication()
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
AzureAdOptions.Settings = options;
});
The implementation of AddAzureAd is as follows: (You’ll see options objects handed around, I have replaced all uses of options with constant values except for ClientId and ClientSecret).
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.AddOpenIdConnect("AzureAd", "Azure AD", options =>
{
var opts = new AzureAdOptions();
configureOptions(opts);
var config = new ConfigureAzureOptions(opts);
config.Configure(options);
});
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(AzureAdOptions azureOptions)
{
_azureOptions = azureOptions;
}
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions) : this(azureOptions.Value) {}
public void Configure(string name, OpenIdConnectOptions options)
{
Configure(options);
}
public void Configure(OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = "https://login.microsoftonline.com/common"; //_azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = "/signin-oidc"; // _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false; // true in production // _azureOptions.RequireHttps;
options.ClientSecret = _azureOptions.ClientSecret;
// Add code for hybridflow
options.ResponseType = "id_token code";
options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
};
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
/// <summary>
/// Redeems the authorization code by calling AcquireTokenByAuthorizationCodeAsync in order to ensure
/// that the cache has a token for the signed-in user.
/// </summary>
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
}
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
throw context.Exception;
}
}
}
public class NaiveSessionCache : TokenCache
{
private static readonly object FileLock = new object();
string UserObjectId = string.Empty;
string CacheId = string.Empty;
ISession Session = null;
public NaiveSessionCache(string userId, ISession session)
{
UserObjectId = userId;
CacheId = UserObjectId + "_TokenCache";
Session = session;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
Load();
}
public void Load()
{
lock (FileLock)
this.Deserialize(Session.Get(CacheId));
}
public void Persist()
{
lock (FileLock)
{
// reflect changes in the persistent store
Session.Set(CacheId, this.Serialize());
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
Session.Remove(CacheId);
}
public override void DeleteItem(TokenCacheItem item)
{
base.DeleteItem(item);
Persist();
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Load();
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
Persist();
}
}

How are bearer tokens stored server-side in Web API 2?

I am setting up bearer token authentication in Web API 2, and I don't understand how (or where) the bearer token is being stored server-side. Here is the relevant code:
Startup:
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static Func<UserManager<IdentityUser>> UserManagerFactory { get; set; }
public static string PublicClientId { get; private set; }
static Startup()
{
PublicClientId = "self";
UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>());
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
}
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseOAuthBearerTokens(OAuthOptions);
}
}
WebApiConfig:
public class WebApiConfig
{
public static void ConfigureWebApi()
{
Register(GlobalConfiguration.Configuration);
}
public static void Register(HttpConfiguration http)
{
AuthUtil.ConfigureWebApiToUseOnlyBearerTokenAuthentication(http);
http.Routes.MapHttpRoute("ActionApi", "api/{controller}/{action}", new {action = Actions.Default});
}
}
AuthUtil:
public class AuthUtil
{
public static string Token(string email)
{
var identity = new ClaimsIdentity(Startup.OAuthOptions.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, email));
var ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
var currentUtc = new SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromMinutes(30));
var token = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
return token;
}
public static void ConfigureWebApiToUseOnlyBearerTokenAuthentication(HttpConfiguration http)
{
http.SuppressDefaultHostAuthentication();
http.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
}
}
LoginController:
public class LoginController : ApiController
{
...
public HttpResponseMessage Post([FromBody] LoginJson loginJson)
{
HttpResponseMessage loginResponse;
if (/* is valid login */)
{
var accessToken = AuthUtil.Token(loginJson.email);
loginResponse = /* HTTP response including accessToken */;
}
else
{
loginResponse = /* HTTP response with error */;
}
return loginResponse;
}
}
Using the above code, I'm able to login and store the bearer token client-side in a cookie, and then make calls to controllers marked with [Authorize] and it lets me in.
My questions are:
Where / how is the bearer token being stored server-side? It seems like this is hapenning through one of the OWIN calls but I can't tell where.
Is it possible to persist the bearer tokens to a database server-side so that they can remain in place after a Web API server restart?
If the answer to #2 is no, is there anyway for a client to maintain its bearer token and re-use it even after the Web API goes down and comes back up? While this may be rare in Production, it can happen quite often doing local testing.
They're not stored server side -- they're issued to the client and the client presents them on each call. They're verified because they're signed by the owin host's protection key. In SystemWeb hosting, that protection key is the machineKey setting from web.config.
That's unnecessary, as long as the protection key the owin host uses doesn't change across server restarts.
A client can hold onto a token for as long as the token is valid.
For those who are looking for how to set web.config, here is a sample
<system.web>
<machineKey validation="HMACSHA256" validationKey="64-hex"
decryption="AES" decryptionKey="another-64-hex"/>
</system.web>
You need both validationKey and decriptionkey to make it work.
And here is how to generate keys
https://msdn.microsoft.com/en-us/library/ms998288.aspx
To add to this, the token can be persisted server side using the SessionStore property of of CookieAuthenticationOptions. I wouldn't advocate doing this but it's there if your tokens become excessively large.
This is an IAuthenticationSessionStore so you could implement your own storage medium.
By default the token is not stored by the server. Only your client has it and is sending it through the authorization header to the server.
If you used the default template provided by Visual Studio, in the Startup ConfigureAuth method the following IAppBuilder extension is called: app.UseOAuthBearerTokens(OAuthOptions).