Openiddict guidance related to external login - asp.net-web-api

I have a mobile app that talks to a backend web API (core 2.0). Presently I have the API configured to use Opendidict with Facebook integration based on the configuration listed below.
public static IServiceCollection AddAuthentication(this IServiceCollection services, AppSettings settings)
{
services.AddOpenIddict<int>(options =>
{
options.AddEntityFrameworkCoreStores<RouteManagerContext>();
options.AddMvcBinders();
options.EnableAuthorizationEndpoint("/auth/authorize");
options.EnableTokenEndpoint("/auth/token");
options.AllowAuthorizationCodeFlow();
options.AllowImplicitFlow();
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(1));
options.SetRefreshTokenLifetime(TimeSpan.FromMinutes(20160));
options.DisableHttpsRequirement();
options.AddEphemeralSigningKey();
});
services.AddAuthentication()
.AddFacebook(o => { o.ClientId = settings.FacebookAppID; o.ClientSecret = settings.FacebookAppSecret; })
.AddOAuthValidation();
return services;
}
The password flow works perfectly when they want to use local account. What I'm struggling with is how to return the access/refresh token after successfully authenticating with Facebook. I have the standard account controller with ExternalLogin and ExternalLoginCallback which also works perfectly as I'm able to successfully login and get the local user account it's tied to and signed in.
In my mind, the user clicks facebook login, which calls ExternalLogincallBack, which logs in the user. After that all I want to do is return the access/refresh token just like the password flow.
When I try to use the ImplicitFlow by providing the implicit flow arguments in the redirect (/auth/authorize?...) from ExternalLoginCallback, I can get the access token, but no refresh token even if I specify the offline_scope. From what I read, it seems the implicit flow doesn't support refresh so I tried code flow.
When using the CodeFlow, I can get the code token from the redirect to "/auth/authorize" but can't figure out how to call into the token endpoint from the authorize endpoint to return the access/refresh token directly to the client app. Do I just need to return the code to the client and have them make another call to post to the token endpoint to get access/refresh tokens?
This doesn't feel correct and I'm stumped. Seems like I should be able to just return the access/refresh token after I've signed in externally just like what happens with password flow. Any help would be greatly appreciated as I've been struggling with this for several days.
[HttpGet("~/auth/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
if (!User.Identity.IsAuthenticated)
{
// If the client application request promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPrompt(OpenIdConnectConstants.Prompts.None))
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIdConnectConstants.Properties.Error] = OpenIdConnectConstants.Errors.LoginRequired,
[OpenIdConnectConstants.Properties.ErrorDescription] = "The user is not logged in."
});
// Ask OpenIddict to return a login_required error to the client application.
return Forbid(properties, OpenIdConnectServerDefaults.AuthenticationScheme);
}
return Challenge();
}
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return BadRequest(new
{
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user, AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties, OpenIdConnectServerDefaults.AuthenticationScheme);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[]
{
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
}
ticket.SetResources("RouteManagerAPI");
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them to a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
continue;
}
var destinations = new List<string>
{
OpenIdConnectConstants.Destinations.AccessToken
};
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
(claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
(claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(destinations);
}
return ticket;
}

When I try to use the CodeFlow, I can get the code token but can't figure out how to call into the token endpoint from the authorize endpoint to return the access/refresh token directly to the client app. Do I just need to return the code to the client and have them make another call to post to the token endpoint to get access/refresh tokens?
That's exactly what you're supposed to do as the code flow is a 2-part process: once your mobile apps has an authorization code, it must redeem it using a simple HTTP call to the token endpoint to get an access token and a refresh token.

Related

How to flow user Consent for a Web API to access MS Graph user profile in AAD V2 end point with MSAL library

I'm trying to build a feature where a client application retrieves the graph resources via WebAPI layer. The scenario has following applications:
Angular5 Client application
ASP.Net Core Web API
The Angular5 client application uses MSAL to authenticate against application (resisted as Converged application via apps.dev.microsoft.com registration application; AAD v2 endpoint).
The authentication flow defines the Web API as scope while login or getting access token
constructor() {
var logger = new Msal.Logger((logLevel, message, piiEnabled) =>
{
console.log(message);
},
{ level: Msal.LogLevel.Verbose, correlationId: '12345' });
this.app = new Msal.UserAgentApplication(
CONFIGSETTINGS.clientId,
null,
this.authCallback,
{
redirectUri: window.location.origin,
cacheLocation: 'localStorage',
logger: logger
}
);
}
public getAPIAccessToken() {
return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
accessToken => {
return accessToken;
},
error => {
return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
accessToken => {
return accessToken;
},
err => {
console.error(err);
}
);
}
);
}
Here scope is defined as scopes: ['api://<<guid of application>>/readAccess']. This is the exact value which was generated when I've registered the Web API in registeration portal. Also, the client application id is added as Pre-authorized applications .
The Web API layer (built in dotnet core -- and uses JwtBearer to validate the authentication), defines the API which internally fetches the graph resources (using HttpClient). To get the access token, I've used following code
public async Task<string> GetAccesToken(string resourceName)
{
var userAssertion = this.GetUserAssertion();
string upn = GetLoggedInUpn();
var userTokenCache = new SessionTokenCache(upn, new Microsoft.Extensions.Caching.Memory.MemoryCache(new MemoryCacheOptions())).GetCacheInstance();
string msGraphScope = "https://graph.microsoft.com/User.Read";
string authority = string.Format("https://login.microsoftonline.com/{0}/v2.0", this.authConfig.TenantId);
ConfidentialClientApplication clientApplication = new ConfidentialClientApplication(this.authConfig.ClientId, authority, new ClientCredential(this.authConfig.AppKey), userTokenCache, null);
var result = await clientApplication.AcquireTokenOnBehalfOfAsync(new string[] { msGraphScope }, userAssertion);
return result != null ? result.AccessToken : null;
}
private UserAssertion GetUserAssertion()
{
string token = this.httpContextAccessor.HttpContext.Request.Headers["Authorization"];
string upn = GetLoggedInUpn();
if (token.StartsWith("Bearer", true, CultureInfo.InvariantCulture))
{
token = token.Trim().Substring("Bearer".Length).Trim();
return new UserAssertion(token, "urn:ietf:params:oauth:grant-type:jwt-bearer");
}
else
{
throw new Exception($"ApiAuthService.GetUserAssertion() failed: Invalid Authorization token");
}
}
Note here, The method AcquireTokenOnBehalfOfAsync is used to get the access token using graph scope. However it throws the following exception:
AADSTS65001: The user or administrator has not consented to use the application with ID '<>' named '<>'. Send an interactive authorization request for this user and resource.
I'm not sure why the of-behalf flow for AAD v2 is not working even when client application uses the Web API as scope while fetching access token and Web API registers the client application as the pre-authorized application.
Note - I've tried using the other methods of ConfidentialClientApplication but even those did not work.
Can someone please point out how the above flow can work without providing the admin consent on Web API?
I've been trying to figure this out for weeks! My solution isn't great (it requires the user to go through the consent process again for the Web API), but I'm not sure that's entirely unexpected. After all, either the Admin has to give consent for the Web API to access the graph for the user, or the user has to give consent.
Anyway, the key was getting consent from the user, which of course the Web API can't do since it has no UI. However, ConfidentialClientApplication will tell you the URL that the user has to visit with GetAuthorizationRequestUrlAsync.
Here's a snippet of the code that I used to get it working (I'm leaving out all the details of propagating the url back to the webapp, but you can check out https://github.com/rlittletht/msal-s2s-ref for a working example.)
async Task<string> GetAuthenticationUrlForConsent(ConfidentialClientApplication cca, string []graphScopes)
{
// if this throws, just let it throw
Uri uri = await cca.GetAuthorizationRequestUrlAsync(graphScopes, "", null);
return uri.AbsoluteUri;
}
async Task<string> GetAccessTokenForGraph()
{
// (be sure to use the redirectUri here that matches the Web platform
// that you added to your WebApi
ConfidentialClientApplication cca =
new ConfidentialClientApplication(Startup.clientId,
"http://localhost/webapisvc/auth.aspx",
new ClientCredential(Startup.appKey), null, null);
string[] graphScopes = {"https://graph.microsoft.com/.default"};
UserAssertion userAssertion = GetUserAssertion();
AuthenticationResult authResult = null;
try
{
authResult = await cca.AcquireTokenOnBehalfOfAsync(graphScopes, userAssertion);
}
catch (Exception exc)
{
if (exc is Microsoft.Identity.Client.MsalUiRequiredException
|| exc.InnerException is Microsoft.Identity.Client.MsalUiRequiredException)
{
// We failed because we don't have consent from the user -- even
// though they consented for the WebApp application to access
// the graph, they also need to consent to this WebApi to grant permission
string sUrl = await GetAuthenticationUrlForConsent(cca, graphScopes);
// you will need to implement this exception and handle it in the callers
throw new WebApiExceptionNeedConsent(sUrl, "WebApi does not have consent from the user to access the graph on behalf of the user", exc);
}
// otherwise, just rethrow
throw;
}
return authResult.AccessToken;
}
One of the things that I don't like about my solution is that it requires that I add a "Web" platform to my WebApi for the sole purpose of being able to give it a redirectUri when I create the ConfidentialClientApplication. I wish there was some way to just launch the consent workflow, get the user consent, and then just terminate the flow (since I don't need a token to be returned to me -- all I want is consent to be granted).
But, I'm willing to live with the extra clunky step since it actually gets consent granted and now the API can call the graph on behalf of the user.
If someone has a better, cleaner, solution, PLEASE let us know! This was incredibly frustrating to research.

AWS Lambda API gateway with Cognito - how to use IdentityId to access and update UserPool attributes?

OK I am now days into this and have made significant progress but am still completely stumped about the fundamentals.
My application uses Cognito User Pools for creating and managing users - these are identified on S3 it seems by their IdentityId. Each of my users has their own S3 folder, and AWS automatically gives them a folder name that is equal to the user's IdentityId.
I need to relate the IdentityId to the other Cognito user information but cannot work out how.
The key thing I need is to be able to identify the username plus other cognito user attributes for a given IdentityId - and it's insanely hard.
So the first battle was to work out how to get the IdentityId when a Cognito user does a request via the AWS API Gateway. Finally I got that worked out, and now I have a Cognito user, who does a request to the API Gateway, and my Lambda function behind that now has the IdentityId. That bit works.
But I am completely stumped as to how to now access the Cognito user's information that is stored in the user pool. I can't find any clear information, and certainly no code, that shows how to use the IdentityId to get the Cognito user's attributes, username etc.
It appears that if I use a "Cognito user pool" to authorize my method in API Gateway, then the body mapping template can be used to put Cognito User information such as the sub and the username and email address into the context, BUT I do NOT get the IdentityId.
BUT if I use the AWS_IAM to authorize my method in the API gateway then the body mapping template does the inverse - it gives me the IdentityId but not the Cognito user fields such as sub and username and email.
It's driving me crazy - how can I get the IdentityId and all the Cognito users fields and attributes together into one data structure? The fact that I seem to be only able to get one or the other just makes no sense.
It turns out that to get the IdentityId AND user details at the same time using AWS Lambda/Cognito/API Gateway, you need to have a Lambda function that is authenticated using AWS_IAM (NOT COGNITO_USER_POOLS), you must send your request the AWS API Gateway, BUT it MUST be a signed request, you must then modify the integration request body mapping templates so that you are given the IdentityId in the event (maybe the context? can't remember). Now you have the IdentityId. Phew. Now you must submit the client's Cognito ID token from the front end to the back end. It is important to validate the token - you cannot trust that it has not been tampered with if you do not validate it. To decode and validate the token you must get the keys from your userpool, put them into your script, ensure that you have jwt decoding libraries plus signature validation libraries included in your AWS lambda zipfile. Now your script must validate the token submitted from the front end and then you can get the user details out of the token. Voila! Now you have both IdentityId plus user details such as their sub, username and email address. So easy.
The above is what is takes to get the username associated with an IdentityId using AWS Cognito/Lambda/API Gateway. This took me days to get working.
Can I please say to any Amazon employees who wander across this ........ well it's WAY too hard to get the user details associated with an IdentityId. You need to fix this. It made me angry that this was so hard and burned so much of my time.
The solution:
I did this by modifying an Amazon employees custom authorizer here:
https://s3.amazonaws.com/cup-resources/cup_custom_authorizer_lambda_function_blueprint.zip
as found and described here:
https://aws.amazon.com/blogs/mobile/integrating-amazon-cognito-user-pools-with-api-gateway/
use strict';
let util = require('util');
var jwt = require('jsonwebtoken');
var jwkToPem = require('jwk-to-pem');
var userPoolId = 'YOUR USERPOOL ID';
var region = 'YOUR REGION'; //e.g. us-east-1
var iss = 'https://cognito-idp.' + region + '.amazonaws.com/' + userPoolId;
//https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html
// DOWNLOAD FROM https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
let userPoolKeys = {PUT YOUR DOWNLOADED USER POOL KEYS JSON HERE};
var pems = {};
let convertKeysToPems = () => {
var keys = userPoolKeys['keys'];
for(var i = 0; i < keys.length; i++) {
//Convert each key to PEM
var key_id = keys[i].kid;
var modulus = keys[i].n;
var exponent = keys[i].e;
var key_type = keys[i].kty;
var jwk = { kty: key_type, n: modulus, e: exponent};
var pem = jwkToPem(jwk);
pems[key_id] = pem;
}
}
exports.handler = function(event, context) {
convertKeysToPems()
console.log(event);
let token = event['body-json'].cognitoUserToken;
console.log(event['body-json'].cognitoUserToken);
ValidateToken(pems, event, context, token);
};
let ValidateToken = (pems, event, context, token) => {
//Fail if the token is not jwt
var decodedJwt = jwt.decode(token, {complete: true});
console.log(decodedJwt)
if (!decodedJwt) {
console.log("Not a valid JWT token");
context.fail("Unauthorized");
return;
}
//Fail if token is not from your UserPool
if (decodedJwt.payload.iss != iss) {
console.log("invalid issuer");
context.fail("Unauthorized");
return;
}
//Reject the jwt if it's not an 'Access Token'
if (decodedJwt.payload.token_use != 'id') {
console.log("Not an id token");
context.fail("Unauthorized");
return;
}
//Get the kid from the token and retrieve corresponding PEM
var kid = decodedJwt.header.kid;
var pem = pems[kid];
if (!pem) {
console.log(pems, 'pems');
console.log(kid, 'kid');
console.log('Invalid token');
context.fail("Unauthorized");
return;
}
//Verify the signature of the JWT token to ensure it's really coming from your User Pool
jwt.verify(token, pem, { issuer: iss }, function(err, payload) {
if(err) {
context.fail("Unauthorized");
} else {
let x = decodedJwt.payload
x.identityId = context.identity.cognitoIdentityId
//let x = {'identityId': context['cognito-identity-id'], 'decodedJwt': decodedJwt}
console.log(x);
context.succeed(x);
}
});
}
This problem -- the problem of using the user's sub instead of their identityId in S3 paths and how to set that up -- is 100% solved for me thanks to the help of #JesseDavda's solution to this problem in this issue: https://github.com/aws-amplify/amplify-js/issues/54
For all of you developers who have been trying to get the identityId in lambdas so that your Amplify default paths in S3 work - this solution simply ends up ignoring identityId altogether - it is a solution that sets up the paths in S3 based on sub instead of the identityId. At the end of this solution, you will never have to deal with more than one id for your users, you will never have to deal with identityId (hopefully) ever again.
If I'm understanding this correctly you want the CognitoIdentityId and the User attributes in the same place. How we do it is the following way:
From the event request context we get the IdentityId:
event.requestContext.identity.cognitoIdentityId
Also from the request context we get the user's sub:
event.requestContext.identity.cognitoAuthenticationProvider.split(':CognitoSignIn:')[1]
Then with the sub you can request the rest of the attributes the following way:
const AWS = require('aws-sdk');
let cognito = new AWS.CognitoIdentityServiceProvider();
let request = {
Username: userSub,
UserPoolId: process.env.userPoolId,
};
let result = await cognito.adminGetUser(request).promise();
const userAttributes = result.UserAttributes.reduce((acc, attribute) => {
const { Name, Value } = attribute;
acc[Name] = Value;
return acc;
}, {});
return userAttributes;

Using Bearer/Jwt authorization without Identity

I'm developing a Web API with Asp 5 and reading some documents about Web API realize I need Bearer authorization.
After searching I can't find any document or sample that use authorization without Aspnet.Identity. I have my own membership and I don't want to use Identity
Should I use Identity library? or is there a way to implement authorization in my membership.
One little side question:
if I'm forced to use Identity how can I change EntityFramework to something like dapper or ADO.NET for my DBContext?
There's already a JWT Bearer middleware, you just need to write something that issues bearer tokens. That's a little more complicated, depending on what you use as your identity store, and as you indicate it's something custom, it's hard to advise on any approach. Creating JWT tokens isn't that hard though;
var now = DateTime.UtcNow;
// Creates new keys automatically, you'd want to store these somewhere
var aes = new AesCryptoServiceProvider();
var signingTokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(
new[]
{
new Claim(JwtRegisteredClaimNames.Aud, "YOURWEBSITEURL") }),
TokenIssuerName = "YourWebSite",
Lifetime = new Lifetime(now, now.AddHours(1)),
SigningCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(aes.Key),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
"http://www.w3.org/2001/04/xmlenc#sha256")
};
var token = signingTokenHandler.CreateToken(tokenDescriptor);
var tokenAsString = signingTokenHandler.WriteToken(token);
None of the authorization pieces depend on membership at all, they'll work with any authentication middleware. None of the documentation for authorization even refers to Identity at all.
There's an authorization workshop available. You can see in the source for that that no-one does identity appear, it's creating user principals on the fly and then storing them in cookies.
To issue your own JWT tokens, you can use OpenIddict:
project.json
{
"dependencies": {
// ...
"AspNet.Security.OAuth.Validation": "1.0.0-*",
"OpenIddict": "1.0.0-*",
"OpenIddict.EntityFrameworkCore": "1.0.0-*",
"OpenIddict.Mvc": "1.0.0-*"
}
}
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store.
options.UseInMemoryDatabase();
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict();
});
services.AddOpenIddict(options =>
{
// Register the Entity Framework stores.
options.AddEntityFrameworkCoreStores<DbContext>();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.AddMvcBinders();
// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");
// Enable the password flow.
options.AllowPasswordFlow();
// During development, you can disable the HTTPS requirement.
options.DisableHttpsRequirement();
});
}
public void Configure(IApplicationBuilder app)
{
// Register the validation middleware, that is used to decrypt
// the access tokens and populate the HttpContext.User property.
app.UseOAuthValidation();
// Register the OpenIddict middleware.
app.UseOpenIddict();
app.UseMvcWithDefaultRoute();
}
}
AuthorizationController.cs
public class AuthorizationController : Controller
{
[HttpPost("~/connect/token"), Produces("application/json")]
public IActionResult Exchange(OpenIdConnectRequest request)
{
if (request.IsPasswordGrantType())
{
// Validate the user credentials.
// Note: to mitigate brute force attacks, you SHOULD strongly consider
// applying a key derivation function like PBKDF2 to slow down
// the password validation process. You SHOULD also consider
// using a time-constant comparer to prevent timing attacks.
if (request.Username != "alice#wonderland.com" ||
request.Password != "P#ssw0rd")
{
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
OpenIdConnectServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
// Add a "sub" claim containing the user identifier, and attach
// the "access_token" destination to allow OpenIddict to store it
// in the access token, so it can be retrieved from your controllers.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject,
"71346D62-9BA5-4B6D-9ECA-755574D628D8",
OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, "Alice",
OpenIdConnectConstants.Destinations.AccessToken);
// ... add other claims, if necessary.
var principal = new ClaimsPrincipal(identity);
// Ask OpenIddict to generate a new token and return an OAuth2 token response.
return SignIn(principal, OpenIdConnectServerDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
}
Request
POST /connect/token HTTP/1.1
Host: localhost:7096
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=alice%40wonderland.com&password=P%40ssw0rd
Response
{
"token_type": "Bearer",
"access_token": "CfDJ8Ec0ZpniaHhGg0e0UUvOH9BWZSGrPoEwGd0_Lq2cse-T29YOq985IBiT5fEe5tTSgY1vxq2Z2ZJ7Ikwlpmh0Lrc4x9pqhqHBziUzsP_rkGZkn47TkNkOkzKCwZJZK5x-irH3HROwClFFTq0rgWdb8rZ2xriffNzsby4VwhxhN5soFD435KzmVYkdv-VuaLYo3QiSuexbRi2USVO9LK30vomAG6h2SAxZ7R-jYsXgf0f5gAmdYxg7w3yicv9v8DpUSBiGGRRfymTOnvGEsFJjGuuP8OlY5qzMs6wGaRWkOvCyV2CK_RZF_3TMs7LYCdMQ-dqWY5A03-03OmP8blKzlrKJMDZfrPQHuysbS931xxy8b3kjicfjNLmMHqzQzbUO4fecm4kY8PFnKozojDtqajfTp2bYhxS65bmVYROrswYeUWEKYR6LSdS1K__IDaLoMlLa-Wf6x1wjM2CchzgqbHRF0KEtdL5Ks88dAS44mp9BM6iUOEWyL7VkbazsBdlNciM5ZZB1_6qunufDW_tcaR8",
"expires_in": 3600
}
For more information, you can read this blog post I wrote about OpenIddict: http://kevinchalet.com/2017/01/30/implementing-simple-token-authentication-in-aspnet-core-with-openiddict/

How to use AddExternalLogin method from WebAPI template

I use Web API template. My target is REST service with Facebook, Twitter, and Google registration and authorization.
I can create account using social networks, but can't add another social network login to existing account.
Specifically, the problem is occurring in this method:
// POST api/Account/AddExternalLogin
[Route("AddExternalLogin")]
public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model)
And even more specifically, the value returned from this call is null,
AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken);
Which explicitly causes the API to return BadRequest("External login failure.");.
The only member in AddExternalLoginBindingModel is ExternalAccessToken, which I'm filling with a social network access token.
How do I add an external login to an existing account? What information should I send to the AddExternalLogin method?
Don't know if I'm doing something conceptually wrong, but I just solved this by avoiding using AccessTokenFormat;
Basically is the same flow as when you login at the first time. But when you get the auth token, then you must set Authorization header ("Bearer " +) and call the adjusted api bellow:
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("AddExternalLogin")]
public async Task<IHttpActionResult> AddExternalLogin() {
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null) {
return InternalServerError();
}
IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), new UserLoginInfo(info.Login.LoginProvider, info.Login.ProviderKey));
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
if (!result.Succeeded) {
return GetErrorResult(result);
}
return Ok();
}

OAuth 2.0. No session? (stateless)

I'm going to implement OAuth 2.0 and REST API with it
to grant different permissions per users and also to scale well.
To scale well, stateless is easier because there is
NO file, database, in-memory based session with it.
Below is how I understand OAuth 2.
OAuth Server give an access token to a user.
The user's access token is stored in cookie.
When user access to REST API, user sends with the access token.
Server receives request with access token.
Server find out whether access token is valid and the user has permission to do request.
Do or reject based on user's privilege.
So I do not have to worry about session storage. Right?
What you are describing here, is the OAuth 2 Implicit Grant flow. OAuth 2 also includes three other flows, but as it seems that your ressource owner (the user) is initiating requests using browser side Javascript (you were talking about cookies), this is the flow you should go for.
On client side, OAuth only requires you to store the access_token for accessing protected ressources (and a refresh_token if you're going for an expiring access_token).
A more recent innovation is JWT - JSON Web Token.
Here is a link to the spec:
JWT - JSON Web Token
JWT is a method of using Hashed tokens using a Hashing method such as HMAC which stands for a Hash-based Message Authentication Code. Because the token is hashed using a secret key, the server can determine if the token has been tampered with.
Here is an example method to create a Hashed token for JWT:
public String createTokenForUser(User user) {
byte[] userBytes = toJSON(user);
byte[] hash = createHmac(userBytes);
final StringBuilder sb = new StringBuilder(170);
sb.append(toBase64(userBytes));
sb.append(SEPARATOR);
sb.append(toBase64(hash));
return sb.toString();
}
Here is an example of decoding a token to ensure it was not tampered with:
public User parseUserFromToken(String token) {
final String[] parts = token.split(SEPARATOR_SPLITTER);
if (parts.length == 2 && parts[0].length() > 0 && parts[1].length() > 0) {
try {
final byte[] userBytes = fromBase64(parts[0]);
final byte[] hash = fromBase64(parts[1]);
boolean validHash = Arrays.equals(createHmac(userBytes), hash);
if (validHash) {
final User user = fromJSON(userBytes);
if (new Date().getTime() < user.getExpires()) {
return user;
}
}
} catch (IllegalArgumentException e) {
//log tampering attempt here
}
}
return null;
}
Here is an article with a more complete example: Stateless Authentication

Resources