I am trying to implement twitter sign in/up. In a asp.net web app, but i am getting 403 http status on the final callback.
I have my callback urls configured in the twitter app portal (I think they are correct)
I give a little bit of context of what i am trying to do
Redict the user to the twitter sining
Then the first callback executes (no issue here) and i call the twitter api to get the user details.
After getting the user details i return a challenge result so i can get the user identity and i specify a second callback for that
The second callback does not execute.
Does somebody can point out to me what am I doing wrong? Or how can i debug the issue?
I am aware that twitter checks that the callback url needs to be set in the app developer portal I got that from this question question
Here's my code and config
app.UseTwitterAuthentication(new TwitterAuthenticationOptions()
{
ConsumerKey = "key",
ConsumerSecret = "qCLLsuS79YDkmr2DGiyjruV76mWZ4hVZ4EiLU1RpZkxOfDqwmh",
Provider = new Microsoft.Owin.Security.Twitter.TwitterAuthenticationProvider
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:twitter:access_token", context.AccessToken));
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:twitter:access_secret", context.AccessTokenSecret));
return Task.FromResult(0);
}
},
BackchannelCertificateValidator = new Microsoft.Owin.Security.CertificateSubjectKeyIdentifierValidator(new[]
{
"A5EF0B11CEC04103A34A659048B21CE0572D7D47", // VeriSign Class 3 Secure Server CA - G2
"0D445C165344C1827E1D20AB25F40163D8BE79A5", // VeriSign Class 3 Secure Server CA - G3
"7FD365A7C2DDECBBF03009F34339FA02AF333133", // VeriSign Class 3 Public Primary Certification Authority - G5
"39A55D933676616E73A761DFA16A7E59CDE66FAD", // Symantec Class 3 Secure Server CA - G4
"add53f6680fe66e383cbac3e60922e3b4c412bed", // Symantec Class 3 EV SSL CA - G3
"4eb6d578499b1ccf5f581ead56be3d9b6744a5e5", // VeriSign Class 3 Primary CA - G5
"5168FF90AF0207753CCCD9656462A212B859723B", // DigiCert SHA2 High Assurance Server CA
"B13EC36903F8BF4701D498261A0802EF63642BC3" // DigiCert High Assurance EV Root CA
}),
});
Calling twitter sign in (I specify the first callback url and this one works )
[AllowAnonymous]
public ActionResult TwitterRegistration()
{
string UrlPath = HttpContext.Request.Url.Authority;
// pass in the consumerkey, consumersecret, and return url to get back the token
NameValueCollection dict = new TwitterClient().GenerateTokenUrl(ConsumerKey, ConsumerSecret, "https://" + UrlPath + "/Account/TwitterRegistrationCallback");
// set a session var so we can use it when twitter calls us back
Session["dict"] = dict;
// call "authenticate" not "authorize" as the twitter docs say so the user doesn't have to reauthorize the app everytime
return Redirect("https://api.twitter.com/oauth/authenticate?oauth_token=" + dict["oauth_token"]);
}
After the callback I call the twitter api to get the user data that works too
[AllowAnonymous]
public ActionResult TwitterRegistrationCallback(string oauth_token, string oauth_verifier)
{
TwitterClient twitterClient = new TwitterClient();
NameValueCollection dict = (NameValueCollection)Session["dict"];
NameValueCollection UserDictionary = HttpUtility.ParseQueryString(twitterClient.GetAccessToken(ConsumerKey, ConsumerSecret, oauth_token, oauth_verifier, dict));
TwitterUserModel twitterUser = JsonConvert.DeserializeObject<TwitterUserModel>(twitterClient.GetTwitterUser(ConsumerKey, ConsumerSecret, UserDictionary));
Session["twitterUser"] = twitterUser;
// Returning challenge not working just redirecting to the action inn case of twitter as we are already authenitcated
return new ChallengeResult("Twitter", Url.Action("ExternalRegistrationCallback", "Account", null));
}
But when I return the Challange result which ends up calling
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
it gives me the exception below (which is the same in the original question)
Here is the callback that is not being called
// GET: /Account/ExternalRegistrationCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalRegistrationCallback()
{
//TODO: Check
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Manage");
}
var loginInfo = await _authenticationManager.GetExternalLoginInfoAsync();
if (Session["twitterUser"] != null)
{
//Workarround for twitter registration callback not using the challenge
loginInfo = new ExternalLoginInfo();
TwitterUserModel twitterUser = (TwitterUserModel)Session["twitterUser"];
loginInfo.Email = twitterUser.email;
}
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Get the information about the user from the external login provider
var info = await _authenticationManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
// Sign in the user with this external login provider if the user already has a login
var result = await _signInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
//User is already registered We show error and tell the user to go back to login page?
return RedirectToLocal((string)Session["ReturnUrl"]);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
//
return RedirectToAction("SendCode", new { ReturnUrl = (string)Session["ReturnUrl"], RememberMe = false });
case SignInStatus.Failure:
default:
// User is authenticated through the previous challange, So here needs to be saved
RegistrationBasicViewModel model = (RegistrationBasicViewModel)Session["RegistrationModel"];
//Check the user is in our db?
ApplicationUser user = _userManager.FindByEmail(loginInfo.Email);
IdentityResult identityResult;
if (user == null)
{
user = new ApplicationUser
{
UserName = loginInfo.Email,
Email = loginInfo.Email,
FirstName = model.FirstName,
LastName = model.LastName,
Nickname = model.Nickname
};
identityResult = await _userManager.CreateAsync(user);
}
else
{
//TODO : Here we might want to tell the user it already exists
identityResult = IdentityResult.Success;
//IdentityResult.Failed(new string[] { "User already registered" });
}
if (identityResult.Succeeded)
{
identityResult = await _userManager.AddLoginAsync(user.Id, info.Login);
if (identityResult.Succeeded)
{
//Adding the branch after te user is sucessfully added
await _signInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
_userBranchService.AddUserBranch(user.Id, model.BranchId);
//Redirect to home page
return RedirectToLocal((string)Session["ReturnUrl"]);
}
}
setPartnerBranchViewBag(model.PartnerId, (string) Session["partner"]);
AddErrors(identityResult);
return View("Register", model );
}
}
Twitter config
[HttpRequestException: Response status code does not indicate success: 403 (Forbidden).]
System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() +223
Microsoft.Owin.Security.Twitter.<ObtainRequestTokenAsync>d__23.MoveNext(
Apparently Owin uses a default url (not the url set on the Challange)
The default url is /signin-twitter So in my case i had to configure https://localhost:44378/signin-twitter as one of the callback urls in the twitter app portal
Even after adding the /signin-twitter to my callback url's, I receive the "Response status code does not indicate success: 403 (Forbidden)." error.
[HttpRequestException: Response status code does not indicate success: 403 (Forbidden).]
System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() +121662
Microsoft.Owin.Security.Twitter.<ObtainRequestTokenAsync>d__23.MoveNext() +2389
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +60
Microsoft.Owin.Security.Twitter.<ApplyResponseChallengeAsync>d__12.MoveNext() +1091
This exception is thrown, even when using the default, out of the box Asp.NET MVC template.
Related
I'm using the Properties column in my OpenIddict applications to store some metadata about an application instead of using a custom entity - comments on this post custom properties within token imply that's what its meant for.
However, despite reading the above post I'm struggling to understand how best to implement this.
Currently my "create application" looks like this:
public interface IOpenIddictAppService
{
Task<CreateOpenIddictAppResponseDto> CreateAsync(CreateOpenIddictAppRequestDto appDto);
}
public class OpenIddictAppService : IOpenIddictAppService
{
private readonly IOpenIddictApplicationManager _appManager;
public OpenIddictAppService(IOpenIddictApplicationManager appManager)
{
_appManager = appManager;
}
public async Task<CreateOpenIddictAppResponseDto> CreateAsync(CreateOpenIddictAppRequestDto appDto)
{
var sha = SHA512.Create();
var clientId = Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())));
var clientSecret = SecureRandomStringHelper.Create(StaticData.ClientSecretSize);
var data = new OpenIddictApplicationDescriptor
{
ClientId = clientId,
ClientSecret = clientSecret,
DisplayName = appDto.Name,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Prefixes.Scope + "api",
OpenIddictConstants.Permissions.ResponseTypes.Code
}
};
// Postman test URL"https://oauth.pstmn.io/v1/callback"
appDto.RedirectUrls.Split(" ").ToList().ForEach(x => data.RedirectUris.Add(new Uri(x)));
data.Properties.Add("IdentityConfig", JsonSerializer.SerializeToElement(new AppIdentityProperties
{
ClientSystemId = appDto.ClientSystemId,
CustomerAccountId = appDto.CustomerAccountId
}));
var app = await _appManager.CreateAsync(data);
CreateOpenIddictAppResponseDto result = new()
{
Id = new Guid(await _appManager.GetIdAsync(app) ?? throw new ArgumentNullException("OpenIddictApplication not found")),
ClientId = clientId, // Send back the client id used
ClientSecret = clientSecret, // Send back the secret used so it can be displayed one-time-only for copy/paste
RedirectUrls = string.Join(" ", await _appManager.GetRedirectUrisAsync(app))
};
return result;
}
}
And I'm loading it like this in my authorization controller:
[HttpPost("~/connect/token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application details cannot be found in the database.");
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens (use the client_id as the subject identifier).
identity.AddClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application))
.AddClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));
var properties = await _applicationManager.GetPropertiesAsync(application);
if (properties.Any(o => o.Key == "IdentityConfig"))
{
var identityConfig = JsonSerializer.Deserialize<AppIdentityProperties>(properties.FirstOrDefault(o => o.Key == "IdentityConfig").Value);
if (identityConfig != null)
{
identity.AddClaim(StaticData.Claims.ClientSystem, identityConfig.ClientSystemId.ToString())
.AddClaim(StaticData.Claims.CustomerAccount, identityConfig.CustomerAccountId.ToString());
}
}
identity.SetDestinations(static claim => claim.Type switch
{
// Allow the "name" claim to be stored in both the access and identity tokens
// when the "profile" scope was granted (by calling principal.SetScopes(...)).
Claims.Name when claim.Subject.HasScope(Scopes.Profile)
=> new[] { Destinations.AccessToken, Destinations.IdentityToken },
// Otherwise, only store the claim in the access tokens.
_ => new[] { Destinations.AccessToken }
});
// Note: In the original OAuth 2.0 specification, the client credentials grant
// doesn't return an identity token, which is an OpenID Connect concept.
//
// As a non-standardized extension, OpenIddict allows returning an id_token
// to convey information about the client application when the "openid" scope
// is granted (i.e specified when calling principal.SetScopes()). When the "openid"
// scope is not explicitly set, no identity token is returned to the client application.
// Set the list of scopes granted to the client application in access_token.
claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetScopes(request.GetScopes());
claimsPrincipal.SetResources(await _scopeManager.ListResourcesAsync(claimsPrincipal.GetScopes()).ToListAsync());
}
else if (request.IsAuthorizationCodeGrantType())
{
// Retrieve the claims principal stored in the authorization code
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the refresh token.
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
}
else
{
throw new InvalidOperationException("The specified grant type is not supported.");
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
Now - it's working...but I have a sneaky feeling that I'm just using it wrong especially based on Kevin Chalet's comments on that post... I think I'm probably also setting the redirect URLs the wrong way too!
Can anyone give me any more specific guidance on how I should really be doing this.
I am facing some issue with Azure AD authentication.
My application architecture is Asp.net MVC Web & Web API as middle ware
when i am trying to authenticate using AD Token at web API from MVC, i am not able to get any error and even no response from WEB API in Code
But if i try accessing the API using browser where i have already used credentials for Authenticating to MVC app it works fine.
Below is the code to access API but it didn't worked
AuthenticationResult result = null;
string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
string ApiClientId = ConfigurationManager.AppSettings["ida:ApiClientId"];
string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:ApplicationURI"];
string postLoginRedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
string clientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
string authority = aadInstance + tenantId;
IConfidentialClientApplication app = MsalAppBuilder.BuildConfidentialClientApplication();
var account = await app.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId());
string[] scopes = { "openid profile offline_access email User.Read" };
try
{
// try to get an already cached token
result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync().ConfigureAwait(false);
}
catch (MsalUiRequiredException ex)
{
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync.
// This indicates you need to call AcquireTokenAsync to acquire a token
//Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
// Build the auth code request Uri
string authReqUrl = await OAuth2RequestManager.GenerateAuthorizationRequestUrl(scopes, app, this.HttpContext, Url);
}
catch (MsalException msalex)
{
Response.Write($"Error Acquiring Token:{System.Environment.NewLine}{msalex}");
}
}
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
HttpClient client = new HttpClient(handler);
//apiUrl client.BaseAddress = apiUrl;
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiUrl + "api/General/GetUserDetailsByEmailAddress?emailAddress=ikhlesh.saxena#amexassetmanagement.com");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
try
{
HttpResponseMessage response = await client.SendAsync(request);
}
catch (Exception ex)
{
}
Check if your scopes allow you to access your API. Also, you need to debug on the API side whether the token is coming with a request, and if yes how it's validated.
I have been testing some code to sign in users to their Microsoft/school/work accounts using raw HttpRequestMessage and HttpResponseMessage. I know there are libraries available to do this but I want to test the raw approach as well (especially usage of refresh tokens), while looking for the right library to handle it.
I'm currently learning authentication, with limited knowledge of ASP.NET/Core.
I'm following this guide: https://learn.microsoft.com/en-us/graph/auth-v2-user
I've just modified the SignIn() method in AccountController in an example project that used more high level libraries to sign in.
I'm requesting an authorization code.
The SignIn() code:
public void SignIn()
{
using (var httpClient = new HttpClient())
{
try
{
var tenant = "my tenant id";
var clientId = ConfigurationManager.AppSettings["ida:AppID"];
var responseType = "id_token+code";
var redirectURI = ConfigurationManager.AppSettings["ida:RedirectUri"];
var responseMode = "form_post";//query";
var appScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
var scopes = $"openid profile offline_access {appScopes}";
var state = "12345";
//var prompt = "consent";
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize", tenant);
var body = string.Format("client_id={1}&response_type={2}&redirect_uri={3}&response_mode={4}&scope={5}&state={6}", tenant, clientId, responseType, redirectURI, responseMode, scopes, state);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).Result;
var content = response.Content.ReadAsStringAsync().Result;
}
catch (Exception ex)
{
}
}
//if (!Request.IsAuthenticated)
//{
// // Signal OWIN to send an authorization request to Azure
// Request.GetOwinContext().Authentication.Challenge(
// new AuthenticationProperties { RedirectUri = "/" },
// OpenIdConnectAuthenticationDefaults.AuthenticationType);
//}
}
I'm just returning void from the method now because I'm not sure what I should return yet.
Debugging and looking at the response variable, the status code is 200, and has some other information to it. However, the content of the HttpResponseMessage, when I paste it into a file and opening it in a browser, displays (or redirects to) https://login.microsoftonline.com/cookiesdisabled, which shows a message saying that I could not be logged in because my browser blocks cookies. However, I don't think this really is the case.
How can I resolve this and have the user log in and consent, and get the authorization code?
I couldn't really find any example in ASP.NET that uses this raw approach. Is it not recommended?
You should fistly understand how OAuth 2.0 authorization code flow works in Azure AD V2.0 :
Microsoft identity platform and OAuth 2.0 authorization code flow
The general process would be like :
When login in client application, user will be redirect to Azure AD login endpoint(https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize) and provides info like which client(client_id) in which tenant(tenant id) user wants to login , and redirect back to which url(redirect_uri) after successful login.
User enter credential , Azure AD validate credential and issue code and redirect user back to redirect url provided in step 1 (Also match one of the redirect_uris you registered in the portal).
The client application will get the code and send http post request with code to acquire access token .
So if you want to manally implement the code flow in your application , you can refer to below code sample :
public async Task<IActionResult> Login()
{
string authorizationUrl = string.Format(
"https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize?response_type=code&client_id={1}&redirect_uri={2}&scope={3}",
"tenantID", "ClientID", "https://localhost:44360/Home/CatchCode",
"openid offline_access https://graph.microsoft.com/user.read");
return Redirect(authorizationUrl);
}
private static readonly HttpClient client = new HttpClient();
public async Task<ActionResult> CatchCode(string code)
{
var values = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "client_id", "XXXXXX"},
{ "code", code},
{ "redirect_uri", "https://localhost:44360/Home/CatchCode"},
{ "scope", "https://graph.microsoft.com/user.read"},
{ "client_secret", "XXXXXXXXXXX"},
};
var content = new FormUrlEncodedContent(values);
//POST the object to the specified URI
var response = await client.PostAsync("https://login.microsoftonline.com/cb1c3f2e-a2dd-4fde-bf8f-f75ab18b21ac/oauth2/v2.0/token", content);
//Read back the answer from server
var responseString = await response.Content.ReadAsStringAsync();
//you can deserialize an Object use Json.NET to get tokens
}
That just is simple code sample which will get Microsoft Graph's access token , you still need to care about url encode and catch exception , but it shows how code flow works .
I have successfully been able to get an access_token (or authenticationToken for Microsoft tokens) using the client side authentication in my Xamarin forms App. I am able to get further user information (email, name, etc.) using the same access token. Now, when I try to pass that token to my Azure Mobile Service backend, I get a 401 error.
Here is my code:
private async System.Threading.Tasks.Task<string> MSGetUserInfo(Account account)
{
// Reference: http://graph.microsoft.io/en-us/docs/overview/call_api
// Note that Microsoft don't recognize the access_token header entry, but rely instead on an Authorization header entry
var client = new HttpClient();
var userInfoRequest = new HttpRequestMessage()
{
RequestUri = new Uri("https://graph.microsoft.com/v1.0/me"),
Method = HttpMethod.Get,
};
// Add acccess Bearer
userInfoRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", account.Properties["access_token"]);
using (var response = await client.SendAsync(userInfoRequest).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
Models.User user = new Models.User();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var jobject = JObject.Parse(responseString);
var userName = (string)jobject["userPrincipalName"];
// Check username is valid
if (String.IsNullOrEmpty(userName))
{
throw new Exception("Username was not set for authenticated user");
}
else
user.ProviderLoginId = userName;
var userDisplayName = (string)jobject["displayName"];
// Replace display name if invalid
if (String.IsNullOrWhiteSpace(userDisplayName))
{
userDisplayName = userName;
}
else
user.Name = userDisplayName;
var userEmail = (string)jobject["mail"];
// Replace email if invalid
if (String.IsNullOrWhiteSpace(userEmail))
{
userEmail = userName;
}
else
user.Email = userEmail;
Valufy.App.currentUser = user;
}
else
{
throw new Exception("OAuth2 request failed: " + await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
}
return "success";
}
The above code snippet works in getting my user details. Now when I try to use the same token in the subsequent call, I get a 404:
public async Task<bool> Authenticate(string token)
{
string message = string.Empty;
var success = false;
JObject objToken = new JObject();
//objToken.Add("access_token", token); //for facebook and google
objToken.Add("authenticationToken", token); //for microsoft
try
{
// Sign in with Facebook login using a server-managed flow.
if (user == null)
{
//ProviderAuth("MICROSOFT");
user = await syncMgr.CurrentClient
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount, objToken);
if (user != null)
{
success = true;
message = string.Format("You are now signed-in as {0}.", user.UserId);
}
}
}
catch (Exception ex)
{
message = string.Format("Authentication Failed: {0}", ex.Message);
}
// Display the success or failure message.
// await new MessageDialog(message, "Sign-in result").ShowAsync();
return success;
}
Is there something that I am doing wrong? Any and all assistance is appreciated.
According to your description, I followed this Git sample about Microsoft Graph Connect Sample for UWP (REST). I could get the access_token and it could work as expected with Microsoft Graph API (e.g. Get a user). But when I use this access_token as the authenticationToken token object for MobileServiceClient.LoginAsync, I could also get 401 Unauthorized.
Then I checked the managed client for Azure Mobile Apps about Authenticate users. For Client-managed authentication flow, I found that the official code sample about using Microsoft Account is working with Live SDK as follows:
// Request the authentication token from the Live authentication service.
// The wl.basic scope should always be requested. Other scopes can be added
LiveLoginResult result = await liveIdClient.LoginAsync(new string[] { "wl.basic" });
if (result.Status == LiveConnectSessionStatus.Connected)
{
session = result.Session;
// Get information about the logged-in user.
LiveConnectClient client = new LiveConnectClient(session);
LiveOperationResult meResult = await client.GetAsync("me");
// Use the Microsoft account auth token to sign in to App Service.
MobileServiceUser loginResult = await App.MobileService
.LoginWithMicrosoftAccountAsync(result.Session.AuthenticationToken);
}
Note: As LiveConnectSession states about AuthenticationToken:
The authentication token for a signed-in and connected user.
While check the authentication with Microsoft Graph, I could only find the access_token instead of AuthenticationToken.
UPDATE:
I have checked LiveLogin for WP8 and Microsoft Account Authentication for Mobile Apps via Fiddler to capture the authorize requests. I found that MS account authentication has the similar authorize request as Live SDK.
I assumed that you need to leverage Live SDK to authenticate the user when using client side authentication with Microsoft account. I found the Live SDK download page is not exist, you could follow the Live SDK for WP8 to get started with Live SDK.
UPDATE2:
For the client-flow authentication (Microsoft Account), you could leverage MobileServiceClient.LoginWithMicrosoftAccountAsync("{Live-SDK-session-authentication-token}"), also you could use LoginAsync with the token parameter of the value {"access_token":"{the_access_token}"} or {"authenticationToken":"{Live-SDK-session-authentication-token}"}. I have tested LoginAsync with the access_token from MSA and retrieve the logged info as follows:
Based on this tutorial http://www.asp.net/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server, I have created an Authorization Server, a Resource Server and a MVC Client.
The MVC Client has a Controller which gets some data from the Resource Server. The Resource Server requires authentication. The MVC Clients gets an authorization code from the Authorization Server and Redirects the user to the Authorization Server for authentication. Finally the MVC Clients exchanges the authorization code for a Access token to Access the Resource Server. This is the Authorization code flow as described by the OAuth 2 protocol. This works fine.
Now, I have the requirement to make a Controller of the MVC Client itself require Authentication. I can not find a tutorial for this.
I added
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
to my Startup.Auth.cs.
I assume, I need to setup the Options to Redirect to the Authorization Server. I can also set the Provider on the Options:
app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()
{
Provider = new OAuthBearerAuthenticationProvider()
});
But I am also stuck on implementing the events of the Provider.
Can anybody guide me in the right direction? Or are there any tutorials which might help me?
I ended up with a solution based on these two articles from Brock Allen:
http://brockallen.com/2013/10/24/a-primer-on-owin-cookie-authentication-middleware-for-the-asp-net-developer/
http://brockallen.com/2014/01/09/a-primer-on-external-login-providers-social-logins-with-owinkatana-authentication-middleware/
The fundemental idea is to register two authentication Middlewares. An active Cookie-Authentication and a passive OAuthBearer-Authentication. In Startup.Auth.cs they are added like this:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/ExternalLogin/Login"),
});
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ExternalBearer,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
});
You also add an ExternalLogin-Controller. Its Login-method has to redirect the user to the Login-page of your Authorization Server to get the authorization code. You have to supply a callback function where you will process the authorization code.
public async Task<ActionResult> Login(string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl) && Request.UrlReferrer != null)
returnUrl = Server.UrlEncode(Request.UrlReferrer.PathAndQuery);
if (Url.IsLocalUrl(returnUrl) && !string.IsNullOrEmpty(returnUrl))
_returnUrl = returnUrl;
//callback function
_redirectUrl = Url.Action("AuthorizationCodeCallback", "ExternalLogin", null, Request.Url.Scheme);
Dictionary<string, string> authorizeArgs = null;
authorizeArgs = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"response_type", "code"}
,{"scope", "read"}
,{"redirect_uri", _redirectUrl}
// optional: state
};
var content = new FormUrlEncodedContent(authorizeArgs);
var contentAsString = await content.ReadAsStringAsync();
return Redirect("http://localhost:64426/oauth/authorize?" + contentAsString);
}
In your callback-function you exchange the authorization code for an access token (plus refresh token) challenge your passive OAuthBearer-authentication Middleware and signin with the Access token as your Cookie.
public async Task<ActionResult> AuthorizationCodeCallback()
{
// received authorization code from authorization server
string[] codes = Request.Params.GetValues("code");
var authorizationCode = "";
if (codes.Length > 0)
authorizationCode = codes[0];
// exchange authorization code at authorization server for an access and refresh token
Dictionary<string, string> post = null;
post = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"client_secret", "ClientSecret"}
,{"grant_type", "authorization_code"}
,{"code", authorizationCode}
,{"redirect_uri", _redirectUrl}
};
var client = new HttpClient();
var postContent = new FormUrlEncodedContent(post);
var response = await client.PostAsync("http://localhost:64426/token", postContent);
var content = await response.Content.ReadAsStringAsync();
// received tokens from authorization server
var json = JObject.Parse(content);
_accessToken = json["access_token"].ToString();
_authorizationScheme = json["token_type"].ToString();
_expiresIn = json["expires_in"].ToString();
if (json["refresh_token"] != null)
_refreshToken = json["refresh_token"].ToString();
//SignIn with Token, SignOut and create new identity for SignIn
Request.Headers.Add("Authorization", _authorizationScheme + " " + _accessToken);
var ctx = Request.GetOwinContext();
var authenticateResult = await ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalBearer);
ctx.Authentication.SignOut(DefaultAuthenticationTypes.ExternalBearer);
var applicationCookieIdentity = new ClaimsIdentity(authenticateResult.Identity.Claims, DefaultAuthenticationTypes.ApplicationCookie);
ctx.Authentication.SignIn(applicationCookieIdentity);
var ctxUser = ctx.Authentication.User;
var user = Request.RequestContext.HttpContext.User;
//redirect back to the view which required authentication
string decodedUrl = "";
if (!string.IsNullOrEmpty(_returnUrl))
decodedUrl = Server.UrlDecode(_returnUrl);
if (Url.IsLocalUrl(decodedUrl))
return Redirect(decodedUrl);
else
return RedirectToAction("Index", "Home");
}
I hope this is useful for someone who is implementing the OAuth authorization code flow in his MVC 5 application.
I used official sample MVC Implicit Client which I believe is the correct authentication flow for MVC application.
For authorization I used this getting started, especially the part about infinite loop when roles are specified [Authorize(Roles = "Foo,Bar")] and user is authenticated but doesn't own any of these.