Can we use same auth cookie (generated in dot net core api) in a different api applications but in the same domain - asp.net-mvc

I have an application(core 2.2.0) which uses a cookie for authentication, below is the code from the Startup.cs file
services.AddAuthentication(cookieConfig.AuthScheme)
.AddCookie(cookieConfig.AuthScheme, options => {
options.LoginPath = new PathString(cookieConfig.LoginPath);
options.AccessDeniedPath = new PathString(cookieConfig.AccessDeniedPath);
options.Cookie = cookie;
options.Events = cookieEvents;
});
And below code is from the Sign-in API
await HttpContext.SignInAsync(_cookieConfig.AuthScheme, userPrincipal, authProps);
Suppose this application generates a cookie 'ABC', and it has a URL1 - https://somedomain.com/api
and I have another API hosted at URL2 - https://somedomain.com/another_api/whatever
But remember both APIs are a different project.
When I pass this cookie to URL1, in the OnAuthorization() I can see the Identities and the Claims properties with correct values of that user and it authorizes the user perfectly. But when I pass the same cookie to URL2 all claims and identity properties show null and it does not authorize.
My question: Is it possible to share authentication between different domains? If yes, then how? If not possible then please suggest an alternative approach.
Additional Details -
We have the above code which generates (at App1) a cookie ABC=some_encrpyted_value.
And in App2 I am trying to unprotect that cookie in below manner
string cookieValue = context.HttpContext.Request.Cookies["ABC"];
var provider = DataProtectionProvider.Create(new DirectoryInfo(#"C:\temp-keys\"));
var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, "ABC", "v2");
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
string plainText = specialUtf8Encoding.GetString(plainBytes);
TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookieValue);
I have placed the data protection key from App1 in the folder 'C:\temp-keys'.
I found this code somewhere in StackOverflow itself and it throws 'CryptographicException: The payload was invalid.' exception at line
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
My understanding of the App1 code is that the Identity and Claim values (with some other values) are encrypted and cookie ABC is generated, and when we send a request this cookie is decrypted and the Identity and Claims are get populated in the context.HttpContext.User
I actually wanted it to work the same way in the App2, I want to pass the cookie and the identity values should be populated in the context.

It is possible. To deal with this type of scenarios we have been using ASP.NET machineKey. That is does is it encrypts/descriptor the auth data using the same key so that different sites can share the same authenticated sessions and data.
In Dotnet Core this machineKey concept has evolved and now called - new data protection system. Microsoft.AspNetCore.DataProtection.SystemWeb package is used to implement the data protection system. To read more on this.
There are various ways how you can store and use the key:
ProtectKeysWithAzureKeyVault (if you are using Azure)
PersistKeysToFileSystem (this is the easier one)
ProtectKeysWith*
UnprotectKeysWithAnyCertificate
Details of the individual scenario is described here.

Related

Initiate and store multiple OAuth2 external authentication challenges in a ASP.NET Core MVC application?

I can authenticate against two separate OAuth authentication schemes but it seems only one can be active at a time. I'd like to compare data from two separate SaaS applications and therefore I need two separate Bearer tokens. How can I initiate multiple OAuth challenges when the user loads the application and then store the Bearer Tokens for each? (e.g. in the Context.User cookie?)
My Startup.cs is as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddScheme1 (options =>
{
options.ClientId = Configuration["Scheme1:ClientId"];
options.ClientSecret = Configuration["Scheme1:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
})
.AddScheme2(options =>
{
options.ClientId = Configuration["Scheme2:ClientId"];
options.ClientSecret = Configuration["Scheme2:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
});...
}
The AuthenticationController calls the Challenge overloaded method from the Microsoft.AspNetCore.Mvc.Core assembly that takes a single provider/scheme (passing multiple schemes in the overloaded method seems to be ignored).
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
...
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
Presumably, you'd prompt the user to sign-into one external application, redirect back to the home page, and then prompt them to sign-into the second one, and then allow them to start using the application proper.
If this is possible - e.g. using a "multiple" Auth cookie - how then would I fetch the correct Bearer token and User values for the given scheme? Currently you just seem to fetch the token with a generic "access_token" name and unique user values:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string userID = User.FindFirstValue(ClaimTypes.NameIdentifier);
There does seem to be some information here regarding using a SignInManager but I'm unable to determine if this is applicable to this problem.
I would aim to start with a standard architecture where the user authenticates with the one and only app, and gets only one set of tokens, issued by your own Authorization Server.
SaaS DATA - OPTION 1
Does the user need to get involved in these connections or can you use a back end to back end flow here?
Your C# code could connect to the SaaS provider with the client credentials grant, using the client ID and secret that you reference above. Provider tokens would then be cached in memory, then used by the back end code to return provider data to the UI. This is a simple option to code.
SaaS DATA - OPTION 2
If the user needs to get involved, because the data is owned by them, you might offer UI options like this. After each click the user is redirected again, to get a token for that provider.
View provider 1 data
View provider 2 data
Aim to emulate the embedded token pattern, where the provider tokens are available as a secondary credential. How you represent this could vary, eg you might prefer to store provider tokens in an encrypted cookie.
CODING AND SIMPLICITY
I would not mix up provider tokens with the primary OAuth mechanism of signing into the app and getting tokens via the .NET security framework, which typically implements OpenID Connect. Instead I would aim to code the SaaS connections on demand.
I think you will find it easier to code the SaaS connections with a library approach, such as Identity Model. This will also help you to deal with SaaS provider differences more easily.
I assume you use OIDC schemes.
First, you need to add two cookie schemes, one for each OIDC authentication scheme as their sign in scheme and set their callback path to different values to stop them competing:
services.AddAuthentication()
.AddCookie("Cookie1")
.AddCookie("Cookie2")
.AddOpenIdConnect("OidcScheme1", opt =>
{
opt.SignInScheme = "Cookie1";
opt.CallbackPath = "/signin-oidc-scheme1";
opt.SaveTokens = true;
})
.AddOpenIdConnect("OidcScheme2", opt =>
{
opt.SignInScheme = "Cookie2";
opt.CallbackPath = "/signin-oidc-scheme2";
opt.SaveTokens = true;
});
This will instruct the OIDC handler to authenticate the user from corresponding cookie.
Second, you need a controller action to challenge the user against each OIDC scheme:
[HttpGet]
[Route("login")]
[AllowAnonymous]
public IActionResult Login([FromQuery]string scheme,
[FromQuery]string? returnUrl)
{
return Challenge(new AuthenticationProperties
{
RedirectUri = returnUrl ?? "/"
}, scheme);
}
From your web app, you need to send the user to the Login endpoint twice with different scheme values:
GET /login?scheme=OidcScheme1
GET /login?scheme=OidcScheme2
Or chain them together using the returnUrl:
GET /login?scheme=OidcScheme1&returnUrl=%2Flogin%3Fscheme%3DOidcScheme2
Once signed in, there should be two cookies in the browser window, for example:
To authenticate the user and restore both identities from two cookies, you can use authorization policy:
[HttpGet]
[Authorize(AuthenticationSchemes = "OidcScheme1,OidcScheme2")]
public async Task<IActionResult> SomeOperation()
{
// Two identities, one from each cookie
var userIdentities = User.Identities;
...
}
To get access token from each authentication scheme, use the method you discovered (GetTokenAsync) and specify authentication scheme:
var token1 = await HttpContext.GetTokenAsync("OidcScheme1", "access_token");
var token2 = await HttpContext.GetTokenAsync("OidcScheme2", "access_token");
It is possible that the access token is not returned from the token endpoint depends on the response_type you used. If this is the case, try set the OpenIdConnectionOptions.ResponseType to OpenIdConnectResponseType.Code and make sure the scope is correct.
I encountered a similar problem where we had microservices that are/were shared across multiple products with each product having a separate IDP tenant (essentially a different token issuer). Perhaps a similar approach might work for your scenario...
The following link helped me with a solution - see here.
Basically I defined a smart authentication scheme
var builder = services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = "smart";
//...
});
Then in the smart policy scheme definition, I decode the JWT coming in to work out the issuer from the iss claim in the JWT, so that I can forward to the correct location for JWT bearer authentication.
builder.AddPolicyScheme("smart", "smart", options =>
{
options.ForwardDefaultSelector = context =>
{
var jwtEncodedString = context.Request.Headers["Authorization"].FirstOrDefault()?.Substring(7);
if (string.IsNullOrEmpty(jwtEncodedString))
return settings.Tenants.First().Key; // There's no authorization header, so just return any.
var token = new JwtSecurityToken(jwtEncodedString: jwtEncodedString);
var issuer = token.Claims.First(c => c.Type == "iss").Value?.TrimEnd('/');
var tenant = settings.Tenants
.Where(pair => pair.Value.Issuer.TrimEnd('/') == issuer)
.Select(pair => pair.Key).FirstOrDefault();
if (tenant == null)
throw new AuthorizationException($"Failed to locate authorization tenant with issuer '{issuer}'.");
return tenant;
};
});
Note: settings.Tenants is just an array of whitelisted tenants (from appsettings) that I configure as follows:
foreach (var tenant in settings.Tenants)
builder.AddJwtBearer(tenant.Key, options => Configure(options, tenant.Value, defaultJwtBearerEvents));

A simple ASP .NET MVC API controller using roles

I wrote a web application using ASP .NET MVC and authorization system by default. I configured IdentityRole and input through external providers. Using the current database I have created my data context. Now I want to write a Xamarin.Android app and connect to my database, I want a simple API. But the feature that you want to access this API was only available to user with a certain role. The API is really very simple and therefore do not want to add to the draft WCF or WebAPI project. How to do it best?
First, you don't need a separate project to use Web Api; you can use both MVC and Web Api in the same project. For one off endpoints for things like in-site AJAX requests, just creating MVC actions that return JSON or XML would be fine, but if you're talking about a true API, even if it's fairly simplistic, I'd say go Web Api.
You'd protect your Web Api actions much the same as you would your MVC actions, using the [Authorize] attribute. If you need to restrict by role, you just pass a role(s) to that. However, the big difference here, especially if you're serving a mobile app, is that you'll need pass the authorization along with the request. That's generally accomplished using the Authorization header along with a bearer token. Basically, you would need to set up an endpoint that signs a user in and returns a token. Then, each subsequent request that needs authorization includes that token in the header.
I want to finish and to fully answer this question and close this topic. I've been searching for how to add the ability for a mobile client to connect to an existing site on ASP.NET MVC. In my search, I came across a great article Justin Hyland on March 2, 2014
In principle, everything in this article is well and clearly written, but I want to make a tiny contribution for clarity.
Under Setup WebAPIConfig stated that the need
added in the following code to the WebApiConfig Register method
But if we consider the case ASP.NET MVC we don't have such file. It's all very simple, you just need such a file to create the folder App_Start. The contents of the file can be left exactly as it is in the article.
To get rid of the bugs which will inevitably appear we need to install two nuget package: Microsoft.AspNet.WebApi and Microsoft.AspNet.WebApi.Owin.
Excellent! Now we can turn to the method to obtain the token and then adding the token to the query we can get the needed data closed by the attribute [Authorize].
A small remark. If You need to access a method which is closed for a specific role that to the Authenticate method from the article should add a few lines of code. Immediately after the line:
identity.AddClaim(new Claim(ClaimTypes.Name, user));
add the line:
identity.AddClaim(new Claim(ClaimTypes.Role, role));
where role you can get the following, for example:
var userIdentity = UserManager.FindAsync(user, password).Result;
var role = RoleManager.FindById(userIdentity.Roles.First().RoleId).Name;
User and password you have to send a request.
I also want to give an example of code which will send request and receive response. To not have to look for and immediately start coding.
async Task<string> GetToken(string userName, string password)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>( "user", userName ),
new KeyValuePair<string, string> ( "password", password )
}
);
using (var client = new HttpClient())
{
HttpResponseMessage response = await client.PostAsync(APP_PATH + "/Authenticate", content);
var result = await response.Content.ReadAsStringAsync();
return result;
}
}
async Task<string> GetUserInfo(string token)
{
using (var client = CreateClient(token))
{
var response = await client.GetAsync(APP_PATH + "/ValidateToken");
return await response.Content.ReadAsStringAsync();
}
}
HttpClient CreateClient(string accessToken = "")
{
var client = new HttpClient();
if (!string.IsNullOrWhiteSpace(accessToken))
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
return client;
}
All have only to call the appropriate methods in the correct order. I hope that is useful to someone.
P.S.
If You create a new project in Visual Studio to get this functionality you just need to tick:

MVC App using Azure AD with ADAL 3 - Authentication Cookie expires after 1 hour

I work on an MVC Web Application using Azure AD with OAuth 2 and Open ID Connect for Authorization of users.
Per documentation tokens are refreshed automatically when a token expires after 60 minutes (which is fine).
Now the problem is, to acquire a token I need to know the currently authenticated user which is stored in a cookie. The code to acquire a Token is like this:
public async Task<AuthenticationToken> GetTokenForApplication(string resourceID)
{
string signedInUserID = ClaimsPrincipal.Current.SignedinUserId();
var tenantID = ClaimsPrincipal.Current.TenantId();
string userObjectID = ClaimsPrincipal.Current.SignedinUserObjectId();
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(Config.ClientId, Config.AppKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(string.Format("{0}{1}", Config.AadInstance, tenantID), new ADALTokenCache(signedInUserID));
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(resourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
var token = new AuthenticationToken(authenticationResult.AccessToken) { ExpiresOn = authenticationResult.ExpiresOn };
return token;
}
Now I am in the dilemma, that the ClaimsPrincipal.Current.SignedinUserId() method call throws a null reference exception. When I inspect the ClaimsPrincipal.Current object, no data about the logged in user is available. But this is the Information needed to renew / request a token.
What is the best practice in an MVC Web App? Is there a way to extend the validity of the cookie or is there any way to reauthenticate the current user without redirecting to the root page of the web application?
After doing more research I have found these two pages which describe some options to deal with my problem pretty good:
Controlling a Web App’s session duration
and ASP.NET-Identity-Cookie-Authentication-Timeouts
are these good approaches?
After doing more research I have found these two pages which describe some options to deal with my problem pretty good:
Controlling a Web App’s session duration
and ASP.NET-Identity-Cookie-Authentication-Timeouts
are these good approaches?

MVC5 app using Azure Active Directory + REST API -- to auth for PowerBI / O365

I'm trying to adapt the WebAPI example shown here, to use in MVC5:
https://msdn.microsoft.com/en-US/library/dn931282.aspx#Configure
I have a regular AccountController based login system, but I also need the user to login via OAuth into PowerBI, so I can pull datasets via the PowerBI REST API. However, I'm gettting the ClaimsPrincipal.Current.FindFirst(..) to be null.
private static async Task<string> getAccessToken()
{
// Create auth context (note: token is not cached)
AuthenticationContext authContext = new AuthenticationContext(Settings.AzureADAuthority);
// Create client credential
var clientCredential = new ClientCredential(Settings.ClientId, Settings.Key);
// Get user object id
var userObjectId = ClaimsPrincipal.Current.FindFirst(Settings.ClaimTypeObjectIdentifier).Value;
// Get access token for Power BI
// Call Power BI APIs from Web API on behalf of a user
return authContext.AcquireToken(Settings.PowerBIResourceId, clientCredential, new UserAssertion(userObjectId, UserIdentifierType.UniqueId.ToString())).AccessToken;
}
It all works fine in the sample app (a WebAPI project). I've also configured the OWIN app.UseOpenIdConnectAuthentication stuff in Startup.Auth.cs.
It seems the issue is the only type of Claim I have in 'ClaimsPrincipal.Current' is a 'CookieAuthentication' - it is missing the http://schemas.microsoft.com/identity/claims/objectidentifier Claim.
Also...the Microsoft OAuth window never opens in the browser...however, the error is within the ActiveDirectory related code...that code shouldn't need an OAuth token in the first place, right?
The recommended way to do this is to use the code that the Open ID Connect middleware will automatically retrieve for you. There is relevant sample here:
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet
This sample uses OAuth to get a token for the AAD Graph API. I don't know PowerBI but I believe that this is exactly analogous to getting a token for PowerBI.
Pay attention in particular to this file:
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/App_Start/Startup.Auth.cs
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
return Task.FromResult(0);
},
The code above is called on every successful authentication, and ADAL is used to retrieve a token to the Graph API. At this point the only reason to get a token for the Graph API is to exchange the short lived auth code for a longer lived refresh token and get that stored in the cache. That is why the 'result' is never used.
Later, in the following file, the cache is employed to retrieve the token and use it to access the graph:
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Controllers/UserProfileController.cs
string tenantId = ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new NaiveSessionCache(userObjectID));
ClientCredential credential = new ClientCredential(clientId, appKey);
result = authContext.AcquireTokenSilent(graphResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
This time the token is actually used.
Substitute PowerBI for Graph API in the sample and I think you should be good to go.
Note that one other thing to pay attention to is the cache implementation. This file contains an appropriately name NaiveSessionCache.
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs
If you have multiple front ends you will need to implement your own, less naive, session cache so that all the front ends can share the same cache.
A potential workaround, at least for me, is to use the "native app" setup on Azure AD and follow this workflow, instead of the web app + oauth workflow:
https://msdn.microsoft.com/en-US/library/dn877545.aspx

External Cookie for External Login in ASP.NET OWIN

We have a legacy system which is built on ASP.NET Mvc 4, now we would like to support Signal Sign On via Azure Active Directory for current users as well as new users. Since we have managed our own authentication workflow, ASP.NET Identity definitely does not fit in our case.
I have managed to build a demo which is working on OWIN OpenIdConnect middleware passive mode without using ASP.NET Identity. The below code works correctly:
app.SetDefaultSignInAsAuthenticationType("ExternalCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ExternalCookie",
AuthenticationMode = AuthenticationMode.Passive,
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Passive,
ClientId = ClientId,
Authority = Authority
// More code
});
And in ExternalLoginCallback action:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var authManager = Request.GetOwinContext().Authentication;
var result = await authManager.AuthenticateAsync("ExternalCookie");
authManager.SignOut("ExternalCookie");
//More code to convert to local identity
}
This case is really common even using other providers like Google, Facebook or Twitter. One thing I have not much clear is ExternalCookie, maybe I have missed the whole thing. My understanding is when external login is successfully, external cookie is used to store the external claim identity. And then we call:
var result = await authManager.AuthenticateAsync("ExternalCookie");
authManager.SignOut("ExternalCookie");
In order to get the external claim identity and then convert external identity to local identity. I have a little bit confusion why we have to call SignOut external cookie in this case.
Also, I'm not sure whether External Cookie is a must when using external login, or do we have other ways around without using External Cookie.
Please someone give an explanation on this point.
To answer your last question, you change the name of cookie in startup.auth file where you configure external cookie -
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
You can use a string instead of DefaultAuthenticationTypes enum and directly specify the name of the cookie like -
app.UseExternalSignInCookie("myExternalCookie");

Resources