How to use AddExternalLogin method from WebAPI template - asp.net-web-api

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();
}

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.

Openiddict guidance related to external login

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.

.NET Web API 2 OWIN Bearer Token Authentication direct call

I have a problem with my Web Api Project.
I have files stored in my Database and want to call them directly in a new window to view/save (URL like : /api/Files/5 - 5 beeing the FileId)
I got everthing working with the Bearer Token for my general AJAX requests with AngularJS for normal Data and it works like a charm. For the file I created a Controller that shows the file in the browser with the corresponding MIME-Type. But now that I changed the action to [Authorize] I get an Access Denied which is correct because I didnt pass an access_token in the HTTP-Header.
I did quite some research if it is possible to pass the Token via the querystring but didn't find anything helpful.
Now my plan is to remove the [Authorize] Attribute from my Controller and try to validate the token myself but I don't know how.
Anyone know how I can get it to work?
I implemented bearer token authentication in my app (AngularJS, WebAPI 2) and I had similar problem - I needed to allow downloading files by clicking on a link. When you click on a link headers are not sent. :(
So, I sent the token value in a query string to download a file
.../mywebapp/api/files/getfile/3?access_token=jaCOTrGsaak6Sk0CpPc1...
and set "Authorization" header to the token value in Startup.Auth.cs. Here is the code:
public void ConfigureAuth(IAppBuilder app)
{
//It needs for file downloads
app.Use(async (context, next) =>
{
if (context.Request.QueryString.HasValue)
{
if (string.IsNullOrWhiteSpace(context.Request.Headers.Get("Authorization")))
{
var queryString = HttpUtility.ParseQueryString(context.Request.QueryString.Value);
string token = queryString.Get("access_token");
if (!string.IsNullOrWhiteSpace(token))
{
context.Request.Headers.Add("Authorization", new[] { string.Format("Bearer {0}", token) });
}
}
}
await next.Invoke();
});
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
This feature is already built in - I wrote about it here:
http://leastprivilege.com/2013/10/31/retrieving-bearer-tokens-from-alternative-locations-in-katanaowin/
For ASP .Net Core I did something like this based on Forward's answer
Extension Method
public static void UseQueryStringBearerValidation(this IApplicationBuilder app)
{
//It needs for file downloads
app.Use(async (context, next) =>
{
if (context.Request.QueryString.HasValue)
{
if (string.IsNullOrWhiteSpace(context.Request.Headers["Authorization"].ToString()))
{
var queryString = QueryHelpers.ParseQuery(context.Request.QueryString.Value);
var token = queryString["access_token"].ToString();
if (!string.IsNullOrWhiteSpace(token))
{
context.Request.Headers.Add("Authorization", new[] {$"Bearer {token}"});
}
}
}
await next();
});
}
Usage
StartUp.cs -> Configure() method
app.UseCustomExceptionHandler();
app.UseQueryStringBearerValidation(); // <-- add before Jwt Handler
app.UseCustomJwtBearerValidation();
app.AddHttpContextProperties();
app.UseStaticFiles();
app.UseMvc(MiddlewareAppConfiguration.AddRouteMappings);
Although I'm not sure it's a very good idea, you could implementing a DelegatingHandler to achieve what you are looking for.
public class QueryStringBearerToken : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var bearerToken = request.GetQueryNameValuePairs()
.Where(kvp => kvp.Key == "bearerToken")
.Select(kvp => kvp.Value)
.FirstOrDefault();
if(!String.IsNullOrEmpty(bearerToken))
{
request.Headers.Add("Authorization", "Bearer " + bearerToken);
}
return base.SendAsync(request, cancellationToken);
}
}
This handler will look for the query string named "bearerToken" and, if it exists, will add it to the request header for the subsequent handlers / filter to process. You might want to check first if the header is already present and not override in this case. You can add this handler in your configuration phase in the usual fashion:
config.MessageHandlers.Insert(0, new QueryStringBearerToken ());
A request for /YourRoute?bearerToken=theToken will pass in the DelegatingHandler, adding the token passed in the query string to the list of headers in the original request and the regular Bearer Token authentication will look for the header and find it.

Google OpenId: Accept only users of a particular company

I am trying to use open id in my application and I have done it successfully with DotNetOpenId.
Now, Google provides service for email and others under the domain of the companies. (Like example#acompany.com). Is there a way to narrow down the authentication to users of a company only?
I know I can do it simply by checking the email address from the response. But I do not think this is a good idea. Its better if the user is NOT authenticated by Google accounts other than that of acompany.com.
Please note that I DONOT know the inside logic of Open Authentication or DotNetOpenId.
Edit
By default Google's openId request prompts https://accounts.google.com/ServiceLogin?...
I can manually change it (in the browser) to https://accounts.google.com/a/iit.du.ac.bd/ServiceLogin?... and it works (iit.du.ac.bd is my school's domain)
I have tried to create request with
Identifier id1 = Identifier.Parse("https://www.google.com/a/iit.du.ac.bd");
Identifier id2= Identifier.Parse("https://www.google.com/a/iit.du.ac.bd/accounts/o8/id");
var openid = new OpenIdRelyingParty();
IAuthenticationRequest request1 = openid.CreateRequest(id1);
IAuthenticationRequest request2 = openid.CreateRequest(id2);
Google's identifier is https://www.google.com/accounts/o8/id"
Edit2
Just found google-apps-openid-url
Your hesitation on using email addresses as your filter is absolutely correct. Follow you instinct. :)
You should filter on OP Endpoint. This will not only assure you that Google is the Provider, but Google has a dedicated OP Endpoint for each individual domain, so you can check that.
IAuthenticationResponse response = relyingParty.GetResponse();
if (response != null) {
if (response.Provider.Uri.AbsoluteUri == "http://google.com/o8/....?domain=yourcompany.com") {
// Allow it
} else {
// Disallow it
}
}
Something like that. You'll have to test to see what the actual URI is for the case you're expecting.
To verify the users email address you would have to ask for it as some point. Either ask before authentication or in the DotNetOpenId request. If you're just going to allow #abcInc.com address and not anyone else I really don't see the justification for using openId at all. You're better of with the default .net membership provider.
Edit: adding the openId code behind
[AllowAnonymous]
[HttpPost]
public ActionResult openIdLogin(FormCollection collection)
{
var openid = new OpenIdRelyingParty();
IAuthenticationRequest aRequest = openid.CreateRequest(Identifier.Parse(collection["openid_identifier"]));
string ReturnUrl = Request.Form["ReturnUrl"];
if (!String.IsNullOrEmpty(ReturnUrl)) {
aRequest.AddCallbackArguments("ReturnUrl", ReturnUrl);
}
var fetch = new FetchRequest();
fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
aRequest.AddExtension(fetch);
return aRequest.RedirectingResponse.AsActionResult();
}
[AllowAnonymous]
public ActionResult openIdLogin(string ReturnUrl)
{
if (ReturnUrl == null) ReturnUrl = "";
var openid = new OpenIdRelyingParty();
IAuthenticationResponse response = openid.GetResponse();
if (response != null)
{
switch (response.Status)
{
case AuthenticationStatus.Authenticated:
ClaimsResponse sreg = response.GetExtension<ClaimsResponse>();
if (sreg != null)
{
sreg.Email; //do something with the email address
}
//codez
break;
case AuthenticationStatus.Canceled:
ModelState.AddModelError("loginIdentifier", "Login was cancelled at the provider");
break;
case AuthenticationStatus.Failed:
ModelState.AddModelError("loginIdentifier", "Login failed using the provided OpenID identifier");
break;
}
}
return View();
}

WebApi authorization filter with token in json payload

I've been looking into Authorization with AspNetWebApi and information is a little sparse on the subject.
I've got the following options:
Pass API token on query string
Pass API token as header
Pass API token using Basic Auth
Pass API token onto the request payload in json.
Which is generally the recommended method?
I'm also wondering for point 4), how would I go about inspecting the json payload in the OnAuthorization method on the AuthorizationFilterAttribute to check whether the API token is correct?
If you want a truly secure option for authorization, something like OAuth is the way to go. This blog post provides a pretty thorough sample using the now obsolete WCF Web API but a lot of the code is salvageable. Or at least, go with using HTTP basic authentication as shown in this blog post. As Aliostad notes, make sure you're using HTTPS if you go the Basic authentication route so the token stays secure.
If you decide you want to roll your own (which almost always will be much less secure than either option above) then below is a code sample of what you'll need for the AuthorizationHanlder if you go HTTP header route. Be aware there's a good chance the way the UserPrinicipal is handled in Web API classes may change so this code is only good for the first preview release. You would need to wire-in the AuthorizationHandler like this:
GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthenticationHandler());
Code for header token:
public class AuthenticationHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var requestAuthTokenList = GetRequestAuthTokens(request);
if (ValidAuthorization(requestAuthTokenList))
{
//TODO: implement a Prinicipal generator that works for you
var principalHelper = GlobalConfiguration.Configuration
.ServiceResolver
.GetService(typeof(IPrincipalHelper)) as IPrincipalHelper;
request.Properties[HttpPropertyKeys.UserPrincipalKey] =
principalHelper.GetPrinicipal(request);
return base.SendAsync(request, cancellationToken);
}
/*
** This will make the whole API protected by the API token.
** To only protect parts of the API then mark controllers/methods
** with the Authorize attribute and always return this:
**
** return base.SendAsync(request, cancellationToken);
*/
return Task<HttpResponseMessage>.Factory.StartNew(
() => new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent("Authorization failed")
});
}
private static bool ValidAuthorization(IEnumerable<string> requestAuthTokens)
{
//TODO: get your API from config or however makes sense for you
var apiAuthorizationToken = "good token";
var authorized = requestAuthTokens.Contains(apiAuthorizationToken);
return authorized;
}
private static IEnumerable<string> GetRequestAuthTokens(HttpRequestMessage request)
{
IEnumerable<string> requestAuthTokens;
if (!request.Headers.TryGetValues("SomeHeaderApiKey", out requestAuthTokens))
{
//Initialize list to contain a single not found token:
requestAuthTokens = new[] {"No API token found"};
}
return requestAuthTokens;
}
}

Resources