MSAL: Getting null everytime when AcquireTokenSilent in ASP.NET MVC Classic - asp.net-mvc

I am working on ASP.Net MVC 4.7.2 (classics). Using Azure AD Authenticaiton (Microsoft Identity platform) for authentication and for Web Api authorization.
When using code flow to get the api resource I wan to store the token and get it silently when needed. I got the code from MSAL team from Git. But code doesn't work. Whenever I acquire the code silently the I get the error. When I debugged the issue I found that IAccounet is return null see the following line that returns null.
IAccount account =
_MsalAppBuilder.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId()).Result;
Since account is null therefore the next line of code throws error. Therefore nt aoo us unable to work
AuthenticationResult result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;
As I debugged the issue, I could not find any reason why it is happening and even after extensive search. However what I found that in the Startup class, the method AcquireAccessToken never hit, thus the token does not save.
Can something help understand it please.
MSAL class:
public static class MsalAppBuilder
{
public static string GetAccountId(this ClaimsPrincipal claimsPrincipal)
{
string oid = claimsPrincipal.GetObjectId();
string tid = claimsPrincipal.GetTenantId();
return $"{oid}.{tid}";
}
private static IConfidentialClientApplication clientapp;
public static IConfidentialClientApplication BuildConfidentialClientApplication()
{
if (clientapp == null)
{
clientapp = ConfidentialClientApplicationBuilder.Create(Globals.clientId)
.WithClientSecret(Globals.clientSecret)
.WithRedirectUri(Globals.redirectUri)
.WithAuthority(new Uri(Globals.authority))
.Build();
// In-memory distributed token cache
clientapp.AddDistributedTokenCache(services =>
{
services.AddDistributedMemoryCache();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = true;
});
});
}
return clientapp;
}
//this was commented already
/*
// Could also use other forms of cache, like Redis
// See https://aka.ms/ms-id-web/token-cache-serialization
clientapp.AddDistributedTokenCache(services =>
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "SampleInstance";
});
});
*/
public static async Task RemoveAccount()
{
BuildConfidentialClientApplication();
var userAccount = await clientapp.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId());
if (userAccount != null)
{
await clientapp.RemoveAsync(userAccount);
}
}
startup class:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
//app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// CookieManager = new SystemWebCookieManager()
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
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 context)
{
var scopes = Globals.scopeTravelAuthApi;
IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { scopes}, context.Code).ExecuteAsync().ConfigureAwait(true);
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
/// <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("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
}
By the way, I have checked all the settings related to Azure AD they ar correct. In the same app I am accessing Graph API which is working fine only issue is getting the access token silently.

My project is .ASP.Net 4.8. In order to fix the issue, I did not update the code, my code remain as is. I just upgraded the NuGet Packages whatever that make sence or needed to be upgraded for ASP.Net 4.8 but specifically the following:
Microsoft.Identity.Client, Microsoft.Identity.Client.Extensions.Msal, Microsoft.Identity.Web.TokenCache, and owin Nuget packages.

Related

How do I create a 'prettier' URL for OpenIddict endpoints in .NET Core

I'm working with an angular front end connected to a .NET Core back end and using OpenIddict for authorization. When I land on my login page, the url looks as follows:
https://localhost:44340/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%3Fresponse_type%3Dcode%26client_id%3DclientIDExample%26state%3DYUVBdDJvUG04SUpVTzZqSEJvRlMxWFZnWU0xSUVsSi1IVnR1WEY2R3pCMG1m%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A4200%26scope%3Dopenid%2520profile%2520email%2520offline_access%26code_challenge%3DAg0TCRqJBaFqpa8sJb--J67Yd88tNPmouGonUvBbBbM%26code_challenge_method%3DS256%26nonce%3DYUVBdDJvUG04SUpVTzZqSEJvRlMxWFZnWU0xSUVsSi1IVnR1WEY2R3pCMG1m
Here is the 'user friendly' url I want the users to see, not the authorize endpoint:
https://localhost:44340/Account/Login
Here is the part of my Authorization code that I'm hitting:
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
// If it can't be extracted, redirect the user to the login page.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result is null || !result.Succeeded)
{
// If the client application requested promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPrompt(Prompts.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
}
Here is my Startup class:
public class Startup
{
public Startup(IConfiguration configuration, IHostEnvironment env)
{
Configuration = configuration;
_env = env;
}
public IConfiguration Configuration { get; }
public IHostEnvironment _env { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration[$"Connections:DefaultConnection"];
var EncryptionCertificate = new X509Certificate2(Convert.FromBase64String(Configuration["EncCert"]), (string)null, X509KeyStorageFlags.MachineKeySet);
var SignCertificate = new X509Certificate2(Convert.FromBase64String(Configuration["SigCert"]), (string)null, X509KeyStorageFlags.MachineKeySet);
services.AddRazorPages();
//DbContext OnConfiguring gets done here
services.AppDataContext(connectionString);
// OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
// (like pruning orphaned authorizations/tokens from the database) at regular intervals.
services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
// Register the Quartz.NET service and configure it to block shutdown until jobs are complete.
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.AddIdentity<ApplicationUserModel, IdentityRole>()
.AddEntityFrameworkStores<DataContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
// Note: to require account confirmation before login,
// register an email sender service (IEmailSender) and
// set options.SignIn.RequireConfirmedAccount to true.
//
// For more information, visit https://aka.ms/aspaccountconf.
options.SignIn.RequireConfirmedAccount = false;
});
services.IdentityServer(EncryptionCertificate, SignCertificate);
services.AddAuthentication(Configuration);
//Adds some claim data
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUserModel>, AdditionalUserClaimsPrincipalFactory>();
services.AddCors(options => options.AddPolicy("AllowCors",
builder =>
{
builder.SetIsOriginAllowed(_ => true)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
})
);
if (_env.IsDevelopment())
{
//Script will populate Database but should be scripted for production
services.AddHostedService<Worker>();
}
}
// 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCors("AllowCors");
//Middleware that takes care of authorization and authentication
//Should always happen before the endpoints
//These methods allow for decorating Controllers with the Authorize attribute, which controls page and feature access
app.UseAuthentication();
app.UseAuthorization();
//Endpoints instead of Razor pages
//Because using APIs and Angular
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();
});
}
}
Do I need a rewrite? A redirect? I need to retain the endpoint but I can't be showing the users the entire endpoint.
I think most system will present you with that 'ugly' link, event if you try to login with Google or Facebook, you will be presented with the same long link.
There are ongoing work in the community that could result in nicer links and one is Pushed Authorization Requests (PAR), but I doubt all token providers supports it and I don't know what the support is for that in ASP.NET Core today.
I ended up having to store the long endpoint in a helper so i could pass the endpoint but get a nice looking ReturnUri, which I'm now passing as 'Home':
New link is:
https://localhost:44340/Account/Login?ReturnUrl=Home
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (_urlHelper != null)
{
_urlHelper.Value.urlEndpoint = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList());
}
// Retrieve the user principal stored in the authentication cookie.
// If it can't be extracted, redirect the user to the login page.
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result is null || !result.Succeeded)
{
// If the client application requested promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPrompt(Prompts.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = "Home"
});
}`

Azure AD B2C - Not redirecting to SignIn/SignUp Page(login.microsoftonline.com...)

I have a project that I have started building, and want to make us of Azure AD B2C - I have followed some tutorials and tested by creating a new MVC app from scratch and I got it working, however, when I try implement it into my existing project, then it does not redirect to the SignIn/SignUp page(this is the login.microsoftonline.com...) url. I know my code works to redirect to this url as it worked on the new project I created to test, so just not sure why it wont on my existing project.
This is in my Web.Config:
<add key="ida:Tenant" value="Name.onmicrosoft.com" />
<add key="ida:ClientId" value="GUID" />
<add key="ida:ClientSecret" value="Secret" />
<add key="ida:AadInstance" value="https://login.microsoftonline.com/tfp/{0}/{1}/v2.0/.well-known/openid-configuration" />
<add key="ida:RedirectUri" value="https://localhost:44382/" />
<add key="ida:SignUpSignInPolicyId" value="B2C_1_SiUpIn" />
<add key="ida:EditProfilePolicyId" value="B2C_1_SiPe" />
<add key="ida:ResetPasswordPolicyId" value="B2C_1_SSPR" />
ActionLink:
#Html.ActionLink("Sign up / Sign in", "SignUpSignIn", "Account", routeValues: null, htmlAttributes: new { id = "signUpSignInLink" })
This is the SignUpSignIn function I am calling:
[AllowAnonymous]
public void SignUpSignIn()
{
// Use the default policy to process the sign up / sign in flow
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge();
return;
}
Response.Redirect("/");
}
Below is the code from my Startup:
public partial class Startup
{
// App config settings
public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
public static string ServiceUrl = ConfigurationManager.AppSettings["api:TaskServiceUrl"];
// B2C policy identifiers
public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
public static string DefaultPolicy = SignUpSignInPolicyId;
// API Scopes
public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope };
// OWIN auth middleware constants
public const string ObjectIdElement = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
// Authorities
public static string Authority = String.Format(AadInstance, Tenant, DefaultPolicy);
/*
* Configure the OWIN middleware
*/
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(AadInstance, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claims to validate
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}"
}
);
}
/*
* On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
* If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
*/
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
{
notification.ProtocolMessage.Scope = OpenIdConnectScopes.OpenId;
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseTypes.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
/*
* Catch any failures received by the authentication middleware and handle appropriately
*/
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
notification.Response.Redirect("/Account/ResetPassword");
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
/*
* Callback function when an authorization code is received
*/
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
// Extract the code from the response notification
var code = notification.Code;
string signedInUserID = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(ClientId, Authority, RedirectUri, new ClientCredential(ClientSecret), userTokenCache, null);
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, Scopes);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
}
When I click on this ActionLink it hits the controller but then does not redirect, it just returns this URL:
https://localhost:44382/account/login?ReturnUrl=%2faccount%2fsignupsignin
One thing I must mention, is I purchased a template that I am using - Dont know if this might have any effect - I am completely stumped and dont know what else I can look at....
If you need anything that I have not posted, please let me know.
If anyone is able to assist me in the right direction, I would greatly appreciate it.
Thanks!
So unfortunately I didnt find a specific solution to FIX the problem, as I could not pin point what exactly the problem was. However, I did sort out the problem by creating a brand new project and moved the items from the template I bought(which was an mvc project) to the newly created project. Pain in the butt as I needed to fix any bugs that occurred because of this, but it ended up working. There was obviously something in the bought template that was causing the issue.
So just to give some context - The template I bought had different frameworks you could use(MVC, PHP, Angular etc.), and I used the MVC project that contained the template and I just carried on building on that MVC project, so I am assuming there was something within that project that was causing the problem.

MVC POST requests losing Authorization header - how to use API Bearer Token once retrieved

I have spent the last week creating an API for an existing MVC application, and am now attempting to secure the API along with reworking the MVC side security as needed.
Currently, the MVC application is set up to use an application cookie via OWIN/OAuth/Identity. I have attempted to incorporate the Bearer token that the Web API is set up to generate whenever making calls to restricted API methods, but have had little success so far - GET requests work just fine, but POST requests are losing the Authorization header when received by the API.
I have created an SDK Client that is being used by the MVC app to make the calls to the API, and have tried a total of three methods of setting the Authorization header for any given call to the API, all of which seem to work just fine for GET requests, but fail completely for any POST requests I need to make...
I can set the Request header in the MVC controller:
HttpContext.Request.Headers.Add("Authorization", "Bearer " + response.AccessToken);
(where response.AccessToken is the token previously retrieved from the API)
I can set the Request header via an extension method on the SDK Client:
_apiclient.SetBearerAuthentication(token.AccessToken)
or I can set the Request header manually on the SDK Client:
_apiClient.Authentication = new AuthenticationHeaderValue("Bearer, accessToken);
(Where accessToken is the token retrieved previously, passed to the Client method being called).
I have very little to go on from this point as to what is causing the issue. The only thing I have been able to glean so far is that ASP.NET causes all POST requests to first send in a request with an Expect header for an HTTP 100-Continue response, after which it will finish the actual POST request. However, it seems that when it does this second request, the Authorization header is no longer present and so the API's Authorize attribute will cause a 401-Unauthorized response instead of actually running the API method.
So, how do I take the Bearer token that I am able to retrieve from the API, and use it on subsequent requests, including the various POST requests that I will need to make?
Beyond that, what is the best way of storing this token on the MVC application itself? I would rather like to avoid having to pass around the string to every method in the application that could need it, but I also have been reading that storing it in a cookie is a very bad idea for security reasons.
A few further points that will be of interest immediately after I get passed this issue:
Does using OAuth Bearer Tokens mean that I can no longer use ApplicationCookies for the MVC application? And/or will it render the following code useless throughout the application?
User.Identity.GetUserId()
Currently I am forced into commenting out my API [Authorize] attributes in order to continue with my work, which obviously isn't ideal but it does allow me to get on with things temporarily.
Startup files:
MVC:
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
private void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ADUIdentityDbContext.Create);
app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);
app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
{
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
//This should be set to FALSE before we move to production.
AllowInsecureHttp = true,
ApplicationCanDisplayErrors = true,
TokenEndpointPath = new PathString("/api/token"),
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ExternalBearer,
CookieName = "ADU",
ExpireTimeSpan = TimeSpan.FromHours(2),
LoginPath = new PathString("/Account/Login"),
SlidingExpiration = true,
});
}
}
API
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.DependencyResolver = new NinjectResolver(new Ninject.Web.Common.Bootstrapper().Kernel);
WebApiConfig.Register(config);
ConfigureOAuth(app);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ADUIdentityDbContext.Create);
app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);
OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/api/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider(),
};
//token generation
app.UseOAuthAuthorizationServer(oAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private IUserBusinessLogic _userBusinessLogic;
/// <summary>
/// Creates the objects necessary to initialize the user business logic field and initializes it, as this cannot be done by dependency injection in this case.
/// </summary>
public void CreateBusinessLogic()
{
IUserRepository userRepo = new UserRepository();
IGeneratedExamRepository examRepo = new GeneratedExamRepository();
IGeneratedExamBusinessLogic examBLL = new GeneratedExamBusinessLogic(examRepo);
_userBusinessLogic = new UserBusinessLogic(userRepo, examBLL);
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { context.Validated(); }
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
//create a claim for the user
ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", user.Id));
context.Validated(identity);
}
}
After a good deal of time working on other aspects of the project, implementing other features has actually made solving this far easier - there is now a Response Wrapper Handler as part of the API, and part of that Handler saves all Headers from the incoming Requests and adds them to the outgoing Responses. I believe this is allowing the ASP.NET MVC side of the application to send the Authorization header again after the 200-OK request is initially sent.
I have modified my authentication in order to take advantage of Roles, but I will attempt to exclude that code as it should not be relevant here:
MVC Startup.cs:
public class Startup
{
public void Configuration(IAppBuilder app) { ConfigureAuth(app); }
/// <summary>
/// Configures authentication settings for OAuth.
/// </summary>
/// <param name="app"></param>
private void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ADUIdentityDbContext.Create);
app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieName = "ADU",
ExpireTimeSpan = TimeSpan.FromHours(2),
LoginPath = new PathString("/Account/Login"),
SlidingExpiration = true
});
}
}
Where it is used (AccountController):
private async Task CreateLoginCookie(AuthorizationToken response, User result)
{
//Create the claims needed to log a user in
//(uses UserManager several layers down in the stack)
ClaimsIdentity cookieIdent = await _clientSDK.CreateClaimsIdentityForUser(response.AccessToken, result, true).ConfigureAwait(false);
if (cookieIdent == null) throw new NullReferenceException("Failed to create claims for cookie.");
cookieIdent.AddClaim(new Claim("AuthToken", response.AccessToken));
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.AllowRefresh = true;
authProperties.IsPersistent = true;
authProperties.IssuedUtc = DateTime.Now.ToUniversalTime();
IOwinContext context = HttpContext.GetOwinContext();
AuthenticateResult authContext = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);
if (authContext != null)
context.Authentication.AuthenticationResponseGrant = new AuthenticationResponseGrant(cookieIdent, authContext.Properties);
//Wrapper methods for IOwinContext.Authentication.SignOut()/SignIn()
SignOut();
SignIn(authProperties, cookieIdent);
}
In my SDK layer, I created a method that I call from the various other methods I use to reach my API in order to set the Authorization for each outgoing Request (I'd like to figure out how to make this into an Attribute, but I'll worry about that later):
private void SetAuthentication()
{
ClaimsIdentity ident = (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
Claim claim;
//Both of these methods (Thread.CurrentPrincipal, and ClaimsPrincipal.Current should work,
//leaving both in for the sake of example.
try
{
claim = ident.Claims.First(x => x.Type == "AuthToken");
}
catch (Exception)
{
claim = ClaimsPrincipal.Current.Claims.First(x => x.Type == "AuthToken");
}
_apiClient.SetBearerAuthentication(claim.Value);
}
API Startup.cs
/// <summary>
/// Configures the settings used by the framework on application start. Dependency Resolver, OAuth, Routing, and CORS
/// are configured.
/// </summary>
/// <param name="app"></param>
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.DependencyResolver = new NinjectResolver(new Bootstrapper().Kernel);
WebApiConfig.Register(config);
ConfigureOAuth(app);
app.UseCors(CorsOptions.AllowAll);
app.UseWebApi(config);
}
/// <summary>
/// Configures authentication options for OAuth.
/// </summary>
/// <param name="app"></param>
public void ConfigureOAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ADUIdentityDbContext.Create);
app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);
OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/api/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider()
};
//token generation
app.UseOAuthAuthorizationServer(oAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
SimpleAuthorizationServerProvider.cs:
/// <summary>
/// Creates an access bearer token and applies custom login validation logic to prevent invalid login attempts.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
// Performs any login logic required, such as accessing Active Directory and password validation.
User user = await CustomLoginLogic(context).ConfigureAwait(false);
//If a use was not found, add an error if one has not been added yet
if((user == null) && !context.HasError) SetInvalidGrantError(context);
//Break if any errors have been set.
if (context.HasError) return;
//create a claim for the user
ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType);
//Add some basic information to the claim that will be used for the token.
identity.AddClaim(new Claim("Id", user?.Id));
identity.AddClaim(new Claim("TimeOf", DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToLongTimeString()));
//Roles auth
SetRoleClaim(user, ref identity);
context.Validated(identity);
}
And finally, the apparent key that wraps everything up together:
public class ResponseWrappingHandler : DelegatingHandler
{
/// <summary>
/// Catches the request before processing is completed and wraps the resulting response in a consistent response wrapper depending on the response returned by the api.
/// </summary>
/// <param name="request">The request that is being processed.</param>
/// <param name="cancellationToken">A cancellation token to cancel the processing of a request.</param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
//Calls Wrapping methods depending on conditions,
//All of the Wrapping methods will make a call to PreserveHeaders()
}
/// <summary>
/// Creates a response based on the provided request with the provided response's status code and request headers, and the provided response data.
/// </summary>
/// <param name="request">The original request.</param>
/// <param name="response">The reqsponse that was generated.</param>
/// <param name="responseData">The data to include in the wrapped response.</param>
/// <returns></returns>
private static HttpResponseMessage PreserveHeaders(HttpRequestMessage request, HttpResponseMessage response, object responseData)
{
HttpResponseMessage newResponse = request.CreateResponse(response.StatusCode, responseData);
foreach (KeyValuePair<string, IEnumerable<string>> header in response.Headers)
newResponse.Headers.Add(header.Key, header.Value);
return newResponse;
}
With all of that in place my project is now able to use authorization/authentication without needing client secrets and such (which was one of the goals of my employer).

How to store bearer tokens when MVC and Web API are in different projects

Situation:
I have a Web API 2 project which acts as an Authorization server (/token endpoint) and a resource server. I am using the template that comes out of box with ASP.Net Web API minus any MVC reference.
The Start.Auth is configured as below:
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
var facebookAuthenticationOptions = new FacebookAuthenticationOptions()
{
AppId = ConfigurationManager.AppSettings["Test_Facebook_AppId"],
AppSecret = ConfigurationManager.AppSettings["Test_Facebook_AppSecret"],
//SendAppSecretProof = true,
Provider = new FacebookAuthenticationProvider
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
return Task.FromResult(0);
}
}
};
facebookAuthenticationOptions.Scope.Add("email user_about_me user_location");
app.UseFacebookAuthentication(facebookAuthenticationOptions);
}
The MVC 5 Client (different Project) uses the Web API app for authorization and data. Below is the code to retrieve the Bearer token in case of Username/Password store:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
model.ExternalProviders = await GetExternalLogins(returnUrl);
return View(model);
}
var client = Client.GetClient();
var response = await client.PostAsync("Token",
new StringContent(string.Format("grant_type=password&username={0}&password={1}", model.Email, model.Password), Encoding.UTF8));
if (response.IsSuccessStatusCode)
{
return RedirectToLocal(returnUrl);
}
return View();
}
Problem
I could retrieve the Bearer token and then add it to the Authorization Header for subsequent calls. I think that would be ok in case of an Angular App or a SPA. But I think there should be something in MVC that handles it for me, like automatically store it in a cookie and send the cookie on subsequent requests. I have searched around quite a lot and there are posts which hint towards this (Registering Web API 2 external logins from multiple API clients with OWIN Identity) but I haven't been able to figure out what to do after I get a token.
Do I need to add something in the MVC app Startup.Auth?
Ideally, I need the functionality which the AccountController in ASP.Net Template (MVC + Web API) gives out of box (Logins, Register, External logins, forget password etc etc...) but with the MVC and Web API in different projects.
Is there a template or a git repo which has this boiler plate code?
Thanks in advance!
Update
Incorporating #FrancisDucharme suggestions, below is the code for GrantResourceOwnerCredentials().
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
//Add a response cookie...
context.Response.Cookies.Append("Token", context.Options.AccessTokenFormat.Protect(ticket));
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
But I can't still seem to get that Cookie or figure out what to do next.
Restating Questions:
What would be the correct way to authenticate, authorize and call Web API methods (Auth and Resource server) from an MVC client?
Is there boilerplate code or template for AccountController which does the basic plumbing (Login, register - internal/external, forgot password etc.)?
You could have your Startup class return a response cookie that the client will then return on all subsequent requests, here's an example. I would do it in GrantResourceOwnerCredentials.
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
//your authentication logic here, if it fails, do this...
//context.SetError("invalid_grant", "The user name or password is incorrect.");
//return;
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
AuthenticationTicket ticket = new AuthenticationTicket(identity);
//Add a response cookie...
context.Response.Cookies.Append("Token", context.Options.AccessTokenFormat.Protect(ticket));
context.Validated(ticket);
}
The Startup class:
public partial class Startup
{
public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }
public Startup()
{
OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
}
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
//I use CORS in my projects....
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
WebApiConfig.Register(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true, //I have this here for testing purpose, production should always only accept HTTPS encrypted traffic.
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new AuthorizationServerProvider()
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(OAuthBearerOptions);
}
}
That assumes the client has cookies enabled, of course.
Then, modify your MVC headers to add the Authorization header to all requests as such.
In the ActionFilterAttribute, fetch your cookie value (Token) and add the header.
Instead of storing in session, I have added it to the the DefaultRequestHeaders as shown below so I don't need to add this in every call I make to Web API.
public async Task AuthenticateUser(string username, string password)
{
var data = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", username),
new KeyValuePair<string, string>("password", password)
});
using (HttpResponseMessage response = await APIClient.PostAsync("/Token", data))
{
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsAsync<AuthenticatedUser>();
APIClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.Access_Token);
}
else
{
throw new Exception(response.ReasonPhrase);
}
}
}

Redirect other then Home/Index using OpenIdConnectAuthentication and Identity server after login

I'm trying to Redirect user to Dashboard but it always redirect it to Home/Index that is because I've set RedirectUri to http://localhost:35641/ in Identity Server Options. But that is true in case of application landing page after login it needs to redirect o dashboard. I can write custom logic in Index's Action Result but I want to avoid it.
MVC web Startup method
public void Configuration(IAppBuilder app)
{
// Implicit mvc owin
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ApplicationConstants.ClientIdNucleusMvcApp,
Authority = ApplicationConstants.UrlBaseAuth,
RedirectUri = ApplicationConstants.UrlBaseWeb,
PostLogoutRedirectUri = ApplicationConstants.UrlBaseWeb,
ResponseType = "id_token token",
Scope = string.Format("openid email {0}", ApplicationScopes.MvcApp),
SignInAsAuthenticationType = "Cookies",
// sample how to access token on form (when adding the token response type)
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
// Adding access token in claims
var accessToken = n.ProtocolMessage.AccessToken;
if (!string.IsNullOrEmpty(accessToken))
{
n.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", accessToken));
}
// Adding identity token in claims
var identityToken = n.ProtocolMessage.IdToken;
if (!string.IsNullOrEmpty(identityToken))
{
n.AuthenticationTicket.Identity.AddClaim(new Claim("identity_token", identityToken));
}
},
RedirectToIdentityProvider = async n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idToken = n.OwinContext.Authentication.User.FindFirst("identity_token");
n.ProtocolMessage.IdTokenHint = idToken == null ? null : idToken.Value;
n.ProtocolMessage.PostLogoutRedirectUri = ApplicationConstants.UrlBaseWeb;
}
}
}
});
}
Here is my Client on Identity Server
new Client
{
Enabled = true,
ClientName = ApplicationConstants.ClientNameNucleusMvcApp,
ClientId = ApplicationConstants.ClientIdNucleusMvcApp,
ClientSecrets = new List<ClientSecret>
{
new ClientSecret(ApplicationConstants.ClientSecretNucleusMvcApp.Sha256())
},
Flow = Flows.Implicit,
RequireConsent = false,
AccessTokenType = AccessTokenType.Reference,
IdentityTokenLifetime = 1800,
AccessTokenLifetime = 1800,
RedirectUris = new List<string>
{
// MVC form post sample
ApplicationConstants.UrlBaseWeb,
ApplicationConstants.UrlBaseWeb + "Dashboard/Index"
},
PostLogoutRedirectUris = new List<string>
{
ApplicationConstants.UrlBaseWeb
}
}
Help will be appreciated. Thanks
The RedirectUri you use for talking with your authority should not make a difference, that's just used for dispatching the token back to your application. After that there is an internal (==local to the app) redirect that is used for setting the session cookie and can go anywhere you want within the site. How do you trigger authentication? If you started from a protected action via [authorize], you should always land back in there in the end. If you are using explicit sign in code like if
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
you can always specify whatever desired landing route you want in RedirectUri. I know, it is fantastically confusing that the property driving this internal redirect has the exact same name as the protocol counterpart - the only excuse we have is that the AuthenticationProperties class already existed when the new claims based middleware was introduced, and calling the actual OAuth/OIDC redirect_uri with the underscore didn't fly with the .NET community. HTH

Resources