I have a webpage which uses multiple URLS for the same application:
for example:
*.MyWebPage.com.au
*.YourWebPage.com.au
So it will use subdomains on multiple urls. The problem is I need to allow for the user to be authenticated on all subdomains of the url which they have logged into.
For example, if they login via www.mywebpage.com.au the cookie needs to be set for *.mywebpage.com.au or if they login via www.yourwebpage.com.au the cookie should be *.yourwebpage.com.au.
Most of the documentation in allowing subdomains for ASP.NET core identity points to the startup.cs (or startup.auth.cs) file and entering something like this:`
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
CookieDomain = "mywebpage.com.au"
});`
this will not work for me because I dont want a fixed domain, I just want to allow for all the users to have access to all the subdomains for the url they have signed in at. I can obviously get their url at the time of login via the request, but I need to dynamically set the cookiedomain at this point.
What I didnt realise when I started was the difference between Identity and CookieAuthentication.
Since I was using Identity
app.UseIdentity();
app.UseCookieAuthentication was not the solution.
I finally found my solution by implementing ICookieManager.
Here is my solution:
in Startup.cs:
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 5;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Cookies.ApplicationCookie.CookieManager = new CookieManager(); //Magic happens here
}).AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
now in a class I have called CookieManager.cs:
public class CookieManager : ICookieManager
{
#region Private Members
private readonly ICookieManager ConcreteManager;
#endregion
#region Prvate Methods
private string RemoveSubdomain(string host)
{
var splitHostname = host.Split('.');
//if not localhost
if (splitHostname.Length > 1)
{
return string.Join(".", splitHostname.Skip(1));
}
else
{
return host;
}
}
#endregion
#region Public Methods
public CookieManager()
{
ConcreteManager = new ChunkingCookieManager();
}
public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
{
options.Domain = RemoveSubdomain(context.Request.Host.Host); //Set the Cookie Domain using the request from host
ConcreteManager.AppendResponseCookie(context, key, value, options);
}
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
{
ConcreteManager.DeleteCookie(context, key, options);
}
public string GetRequestCookie(HttpContext context, string key)
{
return ConcreteManager.GetRequestCookie(context, key);
}
#endregion
In addition to #michael's solution:
ICookie: ICookie Interface is an abstraction layer on top of http cookie object, which secures the data.
ICookieManager: Cookie Manager is an abstraction layer on top of ICookie Interface. This extends the Cookie behavior in terms of <TSource> generic support, Func<TResult>.This is implemented by DefaultCookieManager class. ICookie Interface is a depedenacy of this class.
Usage of CookieManager:
Add CookieManager in startup Configure Service.
Access the CookieManager API.
And the source code is available on git by Nemi Chand.
How many main domains are there? If there are not too many, you can add several CookieAuthenticationOptions. Like this:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "mywebpage.com.au",
CookieDomain = "mywebpage.com.au",
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "yourwebpage.com.au",
CookieDomain = "yourwebpage.com.au",
});
If there are too many main domains, you will need to write your own cookie provider.
Addition to #michael's answer:
How to "handle the deletecookie event, adding options.Domain = RemoveSubdomain(context.Request.Host.Host)":
just add
options.Domain= RemoveSubdomain(context.Request.Host.Host);
before
ConcreteManager.DeleteCookie(context, key, options);
in
CookieManager.DeleteCoockie(..){..};
And don't forget to call CookieManager.DeleteCoockie on logout!
PS Also, if you need to be able to login both on subdomain.example.com and on example.com - you need to modify AppendResponseCookie(..){..}, or you will get only TLD (.com/.ru etc) here
Related
I am newbie in ASP.NET Core, and I have a controller I need to authorise it only on my machine, for the test purposes, however, deny on other...
I have the following config:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.DateFormatString= "yyyy-MM-ddTHH:mm:ssZ";
});
services.AddAuthentication("Cookie")
.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>("Cookie", null);
services.AddLogging(builder => { builder.AddSerilog(dispose: true); });
And on the test controlled I enabled the [Authorise] attrubute
[Authorize]
public class OrderController : Controller
Is there a way to allow my local machine to be autorised to acces the controller's actions? Something like [Authorize(Allow=localhost)]
You can create an action filter like so:
public class LocalhostAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var ip = context.HttpContext.Connection.RemoteIpAddress;
if (!IPAddress.IsLoopback(ip)) {
context.Result = new UnauthorizedResult();
return;
}
base.OnActionExecuting(context);
}
}
And then use the tag Localhost:
//[Authorize]
[Localhost]
public class OrderController : Controller
I believe this will work, restricting the access to the machine where it's executed.
This is more whitelisting than authorization. Authorization means checking whether a user has permission to do something. To do that, the user must be identified first, ie authenticated.
The article Client IP Safelist in the docs shows how you can implement IP safelists through middleware, an action filter or a Razor Pages filter.
App-wide Middleware
The middleware option applies to the entire application. The sample code retrieves the request's endpoint IP, checks it against a list of safe IDs and allows the call to proceed only if it comes from a "safe" list. Otherwise it returns a predetermined error code, in this case 401:
public async Task Invoke(HttpContext context)
{
if (context.Request.Method != "GET")
{
var remoteIp = context.Connection.RemoteIpAddress;
_logger.LogDebug("Request from Remote IP address: {RemoteIp}", remoteIp);
string[] ip = _adminSafeList.Split(';');
var bytes = remoteIp.GetAddressBytes();
var badIp = true;
foreach (var address in ip)
{
var testIp = IPAddress.Parse(address);
if(testIp.GetAddressBytes().SequenceEqual(bytes))
{
badIp = false;
break;
}
}
if(badIp)
{
_logger.LogInformation(
"Forbidden Request from Remote IP address: {RemoteIp}", remoteIp);
context.Response.StatusCode = 401;
return;
}
}
await _next.Invoke(context);
}
The article shows registering it before UseMvc() which means the request will be rejected before reaching the MVC middleware :
app.UseMiddleware<AdminSafeListMiddleware>(Configuration["AdminSafeList"]);
app.UseMvc();
This way we don't waste CPU time routing and processing a request that's going to be rejected anyway. The middleware option is a good choice for implementing a blacklist too.
Action Filter
The filtering code is essentially the same, this time defined in a class derived from ActionFilterAttribute. The filter is defined as a scoped service :
services.AddScoped<ClientIpCheckFilter>();
services.AddMvc(options =>
{
options.Filters.Add
(new ClientIpCheckPageFilter
(_loggerFactory, Configuration));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
In this case the request will reach the MVC infrastructure before it's accepted or rejected.
Razor Pages Filter
The code is once more the same, this time deriving from IPageFilter
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();
}
}
I have a domain that has multiple sites underneath it.
In my Startup.cs I have the following code
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.Cookies.ApplicationCookie.CookieName = "MyAppName";
options.Cookies.ApplicationCookie.ExpireTimeSpanTimeSpan.FromMinutes(300);
})
.AddEntityFrameworkStores<MyDbContext, Guid>()
.AddDefaultTokenProviders();
On the production machine all my sites are in a subfolder under the same site in IIS so I don't want to use the default domain as the cookie name otherwise different sites cookies will have the same name
What I want is to get the current domain e..g mydomain.com and then append it to an explicit cookiename per site that I specify in Startup.cs e.g.
var domain = "get the server domain here somehow e.g. domain.com";
...
options.Cookies.ApplicationCookie.CookieName = "MyAppName." + domain;
How do I do this?
The reason I ask:
I can see in Chrome developer tools the cookies fall under separate domain names of the site I am accessing. However I sometimes somehow still get the situation of when I log into the same site on two different servers, that I can't log into one and it doesn't log any error. The only solution is to use another browser so I can only assume by the symptoms can only be to do with the cookie.
So my question is really just a personal preference question as in my environment I would prefer to append the domain name to the cookie name although the cookie can only belong to a specific domain.
First of all, i would store domain name in configuration. So it would enable me to change it for current environment.
options.Cookies.ApplicationCookie.CookieName = Configuration["DomainName"];
If you don't like this way you can override cookie option on signin event like this(i am not sure below ways are good):
Events = new CookieAuthenticationEvents()
{
OnSigningIn = async (context) =>
{
context.Options.CookieName = "MyAppName." + context.HttpContext.Request.Host.Value.ToString();
}
}
Or catch first request in configure and override options
bool firstRequest = true;
app.Use(async (context, next) =>
{
if(firstRequest)
{
options.CookieName = "MyAppName." + context.HttpContext.Request.Host.Value.ToString();
firstRequest = false;
}
await next();
});
Also see similar question How to get base url without accessing a request
I found this other way. also I have a blog to documented that.
public class Startup
{
public IConfiguration Configuration { get; }
private WebDomainHelper DomainHelper;
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddScoped(sp => new HttpClient() { BaseAddress = new Uri(DomainHelper.GetDomain()) });
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
DomainHelper = new WebDomainHelper(app.ApplicationServices.GetRequiredService());
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(
endpoints =>
{
endpoints.MapControllerRoute("default", "{controller=Account}/{action=Index}/{id?}");
}
);
}
}
public class WebDomainHelper
{
IHttpContextAccessor ContextAccessor;
public WebDomainHelper(IHttpContextAccessor contextAccessor)
{
ContextAccessor = contextAccessor;
}
///
/// Get domain name
///
public string GetDomain()
{
string serverURL;
try
{
serverURL = $"{ContextAccessor.HttpContext.Request.Scheme}://{ContextAccessor.HttpContext.Request.Host.Value}/";
}
catch
{
serverURL = string.Empty;
}
return serverURL;
}
}
Can anybody tell me what is going wrong with this approach for single sing on?
I have a website A with the authentication logic inside. The user can select the role to access the portal that he wants to go. The problem is how can I configure properly those websites to redirect correctly, because when I redirect, I lose the token(redirect is a GET), I never had the cookie on the portal that I want to go. Do I am missing something in my implementation? Maybe a configuration on the portal that I want to redirect? I am not using the audienceUri on the webconfig, Is this related with the problem? I am using a service that gives me a token if the user is authenticated. Then with that token, I want to redirect the page to the corresponding portal.
Said that I will show you the Login Method in AccountController
[HttpPost]
public async Task<ActionResult> Login(string ddlRoles, string ddlUrls, LoginModel user)
{
var service = new AuthenticationServiceAgent(user.Username, user.Password);
var securityService = new SecurityServiceAgent(service.GetToken());
...processing the claims
//AT THIS POINT THE USER IS AUTHENTICATED
FederatedAuthentication.WSFederationAuthenticationModule.SetPrincipalAndWriteSessionToken(token, true);
.... get the url to redirect
Response.Redirect(urlToRedirect, false);
}
My Proxy class looks like this
public class UserNameTokenServiceProxy : TokenServiceProxy
{
#region Properties
public SecurityCredential Credential { get; set; }
#endregion
#region Methods
public override SecurityToken GetToken(ISecurityCredential credential = null)
{
if (null == credential)
{
throw new ArgumentNullException("credential");
}
var factory = new WSTrustChannelFactory(
new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(StsEndPoint)
);
factory.TrustVersion = TrustVersion.WSTrust13;
if (null != factory.Credentials)
{
factory.Credentials.UserName.UserName = credential.UserName;
factory.Credentials.UserName.Password = credential.Password;
}
var rst = new RequestSecurityToken()
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Symmetric,
TokenType = TokenTypes.Saml11TokenProfile11,
***********RelyingPartyUri comes from a config file*********
AppliesTo = new EndpointReference(RelyingPartyUri)
};
try
{
var channel = factory.CreateChannel();
var sToken = channel.Issue(rst);
return sToken;
}
catch (Exception ex)
{
return null;
}
}
#endregion
}
}
And the base class of that one is the following:
public abstract class TokenServiceProxy
{
#region Fields
protected string StsEndPoint;
protected string RelyingPartyUri;
#endregion
#region Constructors
protected TokenServiceProxy()
{
StsEndPoint = ConfigurationManager.AppSettings["stsEndpoint"];
if (null == StsEndPoint)
{
throw new Exception("STSEndPoint cannot be null");
}
RelyingPartyUri = ConfigurationManager.AppSettings["relyingPartyUri"];
if (null == RelyingPartyUri)
{
throw new Exception("RelyingPartyUri cannot be null");
}
//StsEndPoint = <add key="stsEndpoint" value="https://sso.dev.MyCompany.com/idsrv/issue/wstrust/mixed/username"/>;
//RelyingPartyUri = #"value="https://dev.MyCompany.com/MyCompanyPortal"/>";
}
#endregion
#region Abstract Methods
public abstract SecurityToken GetToken(ISecurityCredential credential = null);
#endregion
}
Basically we are not using the default configuration for WS with the audienceUri and the federationConfiguration section, the equivalent would be:
<system.identityModel.services>
<federationConfiguration>
<cookieHandler mode="Default" requireSsl="false" />
<wsFederation passiveRedirectEnabled="true" issuer="https://sso.dev.MyCompany.com/idsrv/issue/wstrust/mixed/username" realm="https://dev.MyCompany.com/MyCompanyPortal" requireHttps="false" />
</federationConfiguration>
</system.identityModel.services>
I'm not quite sure what are all the components in your solution, but in any case you seem to try to do too much on your own.
SSO or not - each integrated application needs to maintain its own authentication state (with authentication cookie), and to do so it needs to get the security token from Identity Provider.
In a most common scenario for WS-Federation (Relying Party initiated SSO), Relying Party application redirects user to Identity Provider, Identity Provider checks user credentials and POSTS security token back to Relying Party application, where it is handled by WS-Federation module to create authentication cookie.
In your case I guess that you are trying to achieve "Identity Provider initiated" SSO (assuming, that your login page is part of Identity Provider).
If this is the case, you need to POST created security token to selected Relying Party (as far as I know WS-Federation does not support sending tokens in GET query string).
Of course you need to have proper WS-Federation configuration on your Relying Party side to accept this token and create authentication cookie - having that the authentication is done automatically by WS-Federation module.
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).