Microsoft Graph API - Manage Booking API Authorization has been denied for this request - microsoft-graph-api

I have a simple ASP.Net web application consist of .aspx web from hosted on azure as cloud service. In my application there is no user login.
I want to connect with Microsoft Graph API and and to use Microsoft Bookings API to get the BookingBusiness collection on my home page load without user login. I am currently debugging my web app on my desktop using Azure emulator.
I have the ofiice 365 premium account access assoiciated with my microsoft account (v-sheeal#microsoft.com) and I had created a Booking business using my v- alias through Booking tools (https://outlook.office.com/owa/?path=/bookings).
I registered an app in AAD in the same tenant with all required permission and provided the Cliend Id and secret in the code to get the access token. I am using Client credentials Grant flow to get the access token and try to invoke the booking API. I am able to get the access token, but when the code try to get the the list of booking businesses it is giving below exception.
DataServiceClientException: {
"error": {
"code": "",
"message": "Authorization has been denied for this request.",
"innerError": {
"request-id": "d0ac6470-9aae-4cc2-9bf3-ac83e700fd6a",
"date": "2018-09-03T08:38:29"
}
}
}
The code and registered app setting details are in below screen shot.
.aspx.cs
private static async Task<AuthenticationResult> AcquireToken()
{
var tenant = "microsoft.onmicrosoft.com";
//"yourtenant.onmicrosoft.com";
var resource = "https://graph.microsoft.com/";
var instance = "https://login.microsoftonline.com/";
var clientID = "7389d0b8-1611-4ef9-a01f-eba4c59a6427";
var secret = "mxbPBS10|[#!mangJHQF791";
var authority = $"{instance}{tenant}";
var authContext = new AuthenticationContext(authority);
var credentials = new ClientCredential(clientID, secret);
var authResult = await authContext.AcquireTokenAsync(resource,
credentials);
return authResult;
}
protected void MSBooking()
{
var authenticationContext = new
AuthenticationContext(GraphService.DefaultAadInstance,
TokenCache.DefaultShared);
var authenticationResult = AcquireToken().Result;
var graphService = new GraphService(
GraphService.ServiceRoot,
() => authenticationResult.CreateAuthorizationHeader());
// Get the list of booking businesses that the logged on user can see.
var bookingBusinesses = graphService.BookingBusinesses; ----- this
line throwing an exception "Authorization has been denied for
this request."
}
GraphService.cs
namespace Microsoft.Bookings.Client
{
using System;
using System.Net;
using Microsoft.OData;
using Microsoft.OData.Client;
public partial class GraphService
{
/// <summary>
/// The resource identifier for the Graph API.
/// </summary>
public const string ResourceId = "https://graph.microsoft.com/";
/// <summary>
/// The default AAD instance to use when authenticating.
/// </summary>
public const string DefaultAadInstance =
"https://login.microsoftonline.com/common/";
/// <summary>
/// The default v1 service root
/// </summary>
public static readonly Uri ServiceRoot = new
Uri("https://graph.microsoft.com/beta/");
/// <summary>
/// Initializes a new instance of the <see
cref="BookingsContainer"/> class.
/// </summary>
/// <param name="serviceRoot">The service root.</param>
/// <param name="getAuthenticationHeader">A delegate that returns
the authentication header to use in each request.</param>
public GraphService(Uri serviceRoot, Func<string>
getAuthenticationHeader)
: this(serviceRoot)
{
this.BuildingRequest += (s, e) => e.Headers.Add("Authorization",
getAuthenticationHeader());
}
}

According to your description, I assume you want to use the Microsoft Bookings API.
Base on the images you’ve provided, You are missing define scope in your code and the Authority is incorrectly.
We can review document to get an Access Token without a user.

Related

How can I allow multiple domains in a .Net Web API with OAuth token authentication using CORS?

We have a .Net Framework Web API, with Token based OAuth authentication, and are trying to make a call to it via an Exchange HTML Add-In. I wish to allow access to several domains, as we may be using several different apps to access it, but we do not wish to allow general (*) access, as it is a proprietary web API, so there is no need for it to be accessed beyond known domains.
I have tried the following in order to satisfy the pre-flight:
Add the Access-Control-Allow-Origin headers with multiple domains via <system.webServer> - this returns a "header contains multiple values" CORS error when including multiple domains
Adding the Access-Control-Allow-Origin headers with multiple domains via a PreflightRequestsHandler : Delegating Handler - same result
If I set these up with one domain, and used the config.EnableCors with an EnableCorsAttribute with the domains, it would add those on to the headers and give an error with redundant domains.
How can I set up my Web API with OAuth and CORS settings for multiple domains?
You can add the header "Access-Control-Allow-Origin" in the response
of authorized sites in Global.asax file
using System.Linq;
private readonly string[] authorizedSites = new string[]
{
"https://site1.com",
"https://site2.com"
};
private void SetAccessControlAllowOrigin()
{
string origin = HttpContext.Current.Request.Headers.Get("Origin");
if (authorizedSites.Contains(origin))
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", origin);
}
protected void Application_BeginRequest()
{
SetAccessControlAllowOrigin();
}
Found the following from Oscar Garcia (#ozkary) at https://www.ozkary.com/2016/04/web-api-owin-cors-handling-no-access.html, implemented it and it worked perfectly! Added to AppOAuthProvider which Microsoft had set up on project creation:
/// <summary>
/// match endpoint is called before Validate Client Authentication. we need
/// to allow the clients based on domain to enable requests
/// the header
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
SetCORSPolicy(context.OwinContext);
if (context.Request.Method == "OPTIONS")
{
context.RequestCompleted();
return Task.FromResult(0);
}
return base.MatchEndpoint(context);
}
/// <summary>
/// add the allow-origin header only if the origin domain is found on the
/// allowedOrigin list
/// </summary>
/// <param name="context"></param>
private void SetCORSPolicy(IOwinContext context)
{
string allowedUrls = ConfigurationManager.AppSettings["allowedOrigins"];
if (!String.IsNullOrWhiteSpace(allowedUrls))
{
var list = allowedUrls.Split(',');
if (list.Length > 0)
{
string origin = context.Request.Headers.Get("Origin");
var found = list.Where(item => item == origin).Any();
if (found){
context.Response.Headers.Add("Access-Control-Allow-Origin",
new string[] { origin });
}
}
}
context.Response.Headers.Add("Access-Control-Allow-Headers",
new string[] {"Authorization", "Content-Type" });
context.Response.Headers.Add("Access-Control-Allow-Methods",
new string[] {"OPTIONS", "POST" });
}

Azure Active Directory ReturnUri is being ignored for IIS child application when returning from Microsoft login

The Backstory
I have an existing ASP.NET MVC5 app that I am wiring up to use oauth2/Azure AD authentication. I actually have this already working in multiple environments (dev/qa/prod/localhost).
In Azure I have an app registration with multiple return URIs, examples:
http://localhost
https://localhost
https://mywebsite.dev.com
https://mywebsite.qa.com
My deployment pipeline sets a config variable to change the ReturnURI for the oauth code and everything is working just fine.
Here is how the code is set up. My internal controllers have an Authorize attribute that, when a user is not authenticated, redirects them to /Account/Index where an oauth challenge is made. Here's how I make the oauth challenge:
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties
{
RedirectUri = Url.Action("ValidateOAuth", "Account", new { returnUrl })
},
OpenIdConnectAuthenticationDefaults.AuthenticationType);
The user is redirected to Microsoft for login and they are returned to /Account/ValidateOAuth. In that action I check Request.IsAuthenticated and if it's true I build a local session variable for extra info about the user and push them into the internal pages.
The Problem
I am running into an issue in a specific scenario. In my QA environment I have two copies of the website hosted. One at the base URI (https://mywebsite.qa.com) and one as a sub-application in IIS located at https://mywebsite.qa.com/staging. This is so that our testers can deploy branches to the /staging site and test them without disrupting our usual QA users.
When a user visits the staging site they are redirected to my /staging/Account/Index action and the challenge redirects them to the Azure AD login site. There, once they authenticate they are redirected back to my site. HOWEVER, they are redirected not to /staging/Account/ValidateOAuth as expected. They are instead redirected to /. This causes them to go through the same authentication cycle of the main QA site.
I am able to reproduce this when running locally by setting up IIS Express to host the site at http://localhost:43000/staging rather than at the base url of http://localhost:43000. I see the exact same behavior where it will redirect to / and I get an error because I have no site hosted there locally.
Here is my Startup.cs with the oauth configuration:
public class Startup
{
// The Client ID is used by the application to uniquely identify itself to Azure AD.
string clientId = ConfigurationManager.AppSettings["aad:ClientId"];
string clientSecret = ConfigurationManager.AppSettings["aad:ClientSecret"];
// RedirectUri is the URL where the user will be redirected to after they sign in.
string redirectUri = ConfigurationManager.AppSettings["aad:RedirectUri"];
// Tenant is the tenant ID (e.g. contoso.onmicrosoft.com, or 'common' for multi-tenant)
static string tenant = ConfigurationManager.AppSettings["aad:Tenant"];
// Authority is the URL for authority, composed by Microsoft identity platform endpoint and the tenant name (e.g. https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0)
string authority = string.Format(CultureInfo.InvariantCulture, ConfigurationManager.AppSettings["aad:Authority"], tenant);
static string graphScopes = ConfigurationManager.AppSettings["aad:GraphScopes"];
/// <summary>
/// Configure OWIN to use OpenIdConnect
/// </summary>
/// <param name="app"></param>
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
/// <summary>
/// Handle failed authentication requests by redirecting the user to the home page with an error in the query string
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
notification.Response.Redirect("/Errors/Error?message=" + notification?.Exception?.Message);
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var secret = clientSecret;
if (string.IsNullOrEmpty(secret))
{
string filePath = HostingEnvironment.MapPath(#"~/local.aadclientsecret.txt");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("AAD Client Secret not found in Web.Config and local.aadclientsecret.txt not found on server!");
}
secret = File.ReadAllText(filePath);
if (string.IsNullOrEmpty(secret))
{
throw new SettingsPropertyNotFoundException("AAD Client not found in Web.Config and local.aadclientsecret.txt was empty!");
}
}
var idClient = ConfidentialClientApplicationBuilder.Create(clientId)
.WithTenantId(tenant)
.WithRedirectUri(redirectUri)
.WithClientSecret(secret)
.Build();
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
string alias = "";
object aliasObj = null;
if (userDetails.AdditionalData.TryGetValue(GraphHelper.Attributes.Alias, out aliasObj))
{
alias = aliasObj.ToString();
}
// Create a new identity and copy all the claims.
// Add in extra claims that are needed.
var id = new ClaimsIdentity(notification.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(notification.AuthenticationTicket.Identity.Claims);
id.AddClaim(new Claim("alias", alias));
notification.AuthenticationTicket = new AuthenticationTicket
(
new ClaimsIdentity(id.Claims, notification.AuthenticationTicket.Identity.AuthenticationType),
notification.AuthenticationTicket.Properties
);
}
catch (MsalException ex)
{
string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Errors/Error?message={message}&&debug={ex.Message}");
}
catch (Microsoft.Graph.ServiceException ex)
{
string message = "GetUserDetailsAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Errors/Error?message={message}&debug={ex.Message}");
}
}
}
If I put breakpoints in the notifications neither are hit (as expected) since the redirect from Azure doesn't even return me to my website hosted at /staging.
Here are my config values (sensitive ones omitted):
<add key="aad:ClientId" value="*************" />
<add key="aad:Tenant" value="***********" />
<add key="aad:ClientSecret" value="***********" />
<add key="aad:Authority" value="https://login.microsoftonline.com/{0}/v2.0" />
<add key="aad:RedirectUri" value="http://localhost:44300" />
<add key="aad:GraphScopes" value="User.Read"/>
Attempted Solution
If I add a new Redirect URI to the Azure app registration to point to http://localhost:44300/staging, it will successfully return me to my locally hosted site at the /staging subdirectory but the Request.IsAuthenticated is always false and I get stuck in an infinite redirect loop. I am sent to Azure to login and it auto-redirects me back to my site which redirects me back to the Azure login page, repeat forever.
I also tried to do this in my live QA environment. I added a Redirect URI in Azure to go to https://mywebsite.qa.com/staging and then altered the RedirectUri in my config to the same. When I visit the /staging site now, it has the same behavior. I get redirected to Azure for login and then back to / instead of /staging/Account/ValidateOAuth.
Help!
I have spent the day searching and shotgun debugging but have no idea what is causing this. What am I doing wrong?
Here's what I ended up doing.
My redirectUri points to the root of my domain https://mywebsite.qa.com.
In Azure, I have multiple redirect URIs, including https://mywebsite.qa.com and https://mywebsite.qa.com/staging.
Then, in my Account controller where I handle login, I have this bit of code that will redirect users to Azure for login:
if (!Request.IsAuthenticated || forceLogin)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties
{
RedirectUri = Url.Action("ValidateOAuth", "Account", new { returnUrl })
},
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
else
{
HttpContext.Response.Redirect(Url.Action("ValidateOAuth", "Account"));
}
I'm not sure what's different now between what I was doing before and what I'm doing now since it's been 3 months since I had the issue.
FYI, in the ValidateOAuth action I just check IsAuthenticated and then do some other maintenance stuff (like looking up the user in the Db, etc) before forwarding the user to the site's internals.

MS Graph - Getting App Token (Console Application)

Console Application - C# .Net 4.6
Dedicated Admin user - I can't have it prompt every time for a login - must run unattended as a native commandline/console application.
I am simply trying to get bearer token to send along with the Graph SDK calls.
I get a token (the same one every time) but am told it's expired. Here is the message:
Access Token Expired, Use Access & Refresh Tokens to Validate
Since this is a console application I do not know how I can get/keep the access and refresh tokens to do this.
FYI: Earlier effort I followed the steps Getting Access Without a User: https://developer.microsoft.com/en-us/graph/docs/concepts/auth_v2_service I could not get past the simple Token HTTP request on that page: unauthorized.
This is my latest effort. Any help would be welcome:
using Microsoft.Graph;
using Microsoft.Identity.Client;
public static async Task<string> GetTokenForAppAsync()
{
if (TokenForApplication == null || TokenForApplicationExpiration <= DateTimeOffset.UtcNow.AddMinutes(5))
{
TokenCacheUser = null;
TokenCacheApplication = null;
ConfidentialClientApplication cl = new ConfidentialClientApplication(Settings.AuthClientId,
returnUrl,
new ClientCredential(Settings.AuthClientSecret),
TokenCacheUser,
TokenCacheApplication);
AuthenticationResult authResult = cl.AcquireTokenForClientAsync(new string[] { "https://graph.microsoft.com/.default" }, true).Result;
TokenForApplication = authResult.AccessToken;
Console.WriteLine(authResult.AccessToken);
}
return TokenForApplication;
}
I am open to any solutions which utilize the MS Graph and Identity Libs.
graph_authentication_example
This is an example of token based authentication for a console application. The application must be run at least one time at which you will be prompted to signin but once that is complete an authentication token is stored on the machine the application runs from.
I run a console application as a task from our server and access the Graph API to get various ActiveDirectory data sets using the Graph Endpoints - Typically I need to login once published and then it runs afterwards - this is in the testing phase just now but seems to work well.
Dependencies:
Must have an Azure Active Directory user which will be used for the login and subsequent authentication. Everything happens in the context of this user.
The following Nuget packages are used:
Microsoft.Graph >= v1.6.2
Microsoft.Graph.Core >= v1.6.2
Microsoft.Identity.Client >= v1.1.0 preview
Microsoft.IdentityModel.Clients.ActiveDirectory >= v3.17.1
Newtonsoft.Json >= v1.0.3
System.Net.Http >= v4.3.3
System.Security.Cryptography.Algorithms >= v4.3.0
System.Security.Cryptography.Encoding >= v4.3.0
System.Security.Cryptography.Primitives >= v4.3.0
You must create an application here [https://apps.dev.microsoft.com/] under same user created above, this will give you your client/app id.
You can see Graph in action here [https://developer.microsoft.com/en-us/graph/graph-explorer/] and login with the user created above to test against your own Azure Active Directory.
Settings
I used a .ini file for storing settings but the values are valid, comments in .ini style - note that there are {name} style text in certain entries, those are for string replacement.
You will see Settings.SomeName - that maps to the following:
[Endpoint]
; we don't want v1.0 because of our needs but it is valid
GraphVersion = beta ; v1.0 or beta
; Common Graph endpoint - we sub version
GraphEndpoint = https://graph.microsoft.com/{version}
[Auth]
; authentication uri
Uri = https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
; authority uri
Authority = https://login.microsoftonline.com/{tenant}
; if we need to login or re-login
RedirectUri = https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient
; you may have a GUID style tenant but 'common' worked fine since it auth's back to Azure anyway
Tenant = common
; the scopes we needed with Graph, yours may vary
Scopes = { User.ReadBasic.All, User.Read.All, User.ReadWrite.All, Directory.AccessAsUser.All, Directory.Read.All, Directory.ReadWrite.All, Group.ReadWrite.All }
; the id of your azure application - guid
ClientId = xxxx###-2##8-4##9-b##1-ec#########8f2
GrantType = client_credentials
Code Snippets
I tried to include complete functions and indicate separation by indicating which files they come from. These are the main parts for token authentication - mostly code complete with exception to 'private' code.
Starting point: Let's say that in my Program.cs I have a call to the following function, everything flows from here for authentication:
// we are just getting a group by id - CreateAuthenticatedClient() is called before every call to Graph
public static async Task<Group> GetGroupAsync(string groupId)
{
var graphClient = AuthenticationHelper.CreateAuthenticatedClient();
try
{
var group = await graphClient.Groups[groupId].Request().GetAsync();
if (group == null) return null;
return group;
}
catch (ServiceException e)
{
ConsoleHelper.WriteException($"GetGroupAsync.{ServiceErrorString(e)}");
return null;
}
}
AuthenticationHelper.cs - complete class
public class AuthenticationHelper
{
public static string TokenForUser = null;
public static DateTimeOffset TokenForUserExpiration;
// this is the 'magic' where we get the user cache
public static PublicClientApplication IdentityClientApp = new PublicClientApplication(Settings.AuthClientId, Settings.AuthAuthority, TokenCacheHelper.GetUserCache());
private static GraphServiceClient graphClient = null;
// Get an access token for the given context and resourceId. An attempt is first made to
// acquire the token silently. If that fails, then we try to acquire the token by prompting the user.
public static GraphServiceClient CreateAuthenticatedClient()
{
if (graphClient == null)
{
try
{
graphClient = new GraphServiceClient(
Settings.GraphEndpoint,
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
var token = await GetTokenForUserAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
requestMessage.Headers.Add("azure-graph-test", "manage group membership");
}));
return graphClient;
}
catch (ServiceException sex)
{
Console.WriteLine($"Could not create a graph client service: {sex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Could not create a graph client: {ex.Message}");
}
}
return graphClient;
}
/// <summary>
/// get Token for User
/// </summary>
public static async Task<string> GetTokenForUserAsync()
{
if (TokenForUser == null || TokenForUserExpiration <= DateTimeOffset.UtcNow.AddMinutes(5))
{
AuthenticationResult authResult;
try
{
authResult = await IdentityClientApp.AcquireTokenSilentAsync(Settings.AuthScopes, IdentityClientApp.Users.FirstOrDefault());
TokenForUser = authResult.AccessToken;
}
catch (Exception)
{
if (TokenForUser == null || TokenForUserExpiration <= DateTimeOffset.UtcNow.AddMinutes(5))
{
authResult = await IdentityClientApp.AcquireTokenAsync(Settings.AuthScopes);
TokenForUser = authResult.AccessToken;
TokenForUserExpiration = authResult.ExpiresOn;
}
}
}
return TokenForUser;
}
}
TokenCacheHelper.cs - this is a microsoft class that is key to getting/setting the token cache
// Copyright (c) Microsoft Corporation.
// All rights reserved.
// This code is licensed under the MIT License.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
static class TokenCacheHelper
{
/// <summary>
/// Get the user token cache
/// </summary>
public static TokenCache GetUserCache()
{
if (usertokenCache == null)
{
usertokenCache = new TokenCache();
usertokenCache.SetBeforeAccess(BeforeAccessNotification);
usertokenCache.SetAfterAccess(AfterAccessNotification);
}
return usertokenCache;
}
static TokenCache usertokenCache;
/// <summary>
/// Path to the token cache
/// </summary>
public static string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + "msalcache.txt";
private static readonly object FileLock = new object();
public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
args.TokenCache.Deserialize(File.Exists(CacheFilePath)
? File.ReadAllBytes(CacheFilePath)
: null);
}
}
public static void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (args.TokenCache.HasStateChanged)
{
lock (FileLock)
{
// reflect changes in the persistent store
File.WriteAllBytes(CacheFilePath, args.TokenCache.Serialize());
// once the write operationtakes place restore the HasStateChanged bit to filse
args.TokenCache.HasStateChanged = false;
}
}
}
}

How do I convert an external OAuth Identity into a local identity in Umbraco?

I am attempting to develop a proof-of-concept using my company's website as an OAuth authorization server to be consumed by Umbraco via OWIN/Katana. All of the OAuth plumbing appears to be working just fine but Umbraco isn't converting the external identity into a local identity. Instead of being logged into the Umbraco backend, the user lands back on the login page. The only change once the OAuth flow has completed is that Umbraco has created an UMB_EXTLOGIN cookie containing a long encrypted string.
If I login using a local identity directly (i.e. user name and password on the Umbraco backend login page) Umbraco creates 4 cookies: UMB_UCONTEXT, UMB_UPDCHK, XSRF-TOKEN and XSRF-V. I assume I'm missing something that converts the external identity into a local one, but I'm not sure what that is.
Startup.Auth.cs
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.ConfigureBackOfficeMyCompanyAuth(Properties.Settings.Default.ClientId, Properties.Settings.Default.ClientSecret);
}
}
UmbracoMyCompanyAuthExtensions.cs
public static class UmbracoMyCompanyAuthExtensions
{
public static void ConfigureBackOfficeMyCompanyAuth(this IAppBuilder app, string clientId, string clientSecret,
string caption = "My Company", string style = "btn-mycompany", string icon = "fa-rebel")
{
var options = new MyCompanyAuthenticationOptions
{
ClientId = clientId,
ClientSecret = clientSecret,
SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType,
Provider = new MyCompanyAuthenticationProvider(),
CallbackPath = new PathString("/MyCompanySignIn")
};
options.ForUmbracoBackOffice(style, icon);
options.Caption = caption;
app.UseMyCompanyAuthentication(options);
}
}
MyCompanyAuthenticationExtension.cs
public static class MyCompanyAuthenticationExtensions
{
public static IAppBuilder UseMyCompanyAuthentication(this IAppBuilder app, MyCompanyAuthenticationOptions options)
{
if (app == null)
{
throw new ArgumentNullException("app");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
app.Use(typeof(MyCompanyAuthenticationMiddleware), new object[] { app, options });
return app;
}
public static IAppBuilder UseMyCompanyAuthentication(this IAppBuilder app, string clientId, string clientSecret)
{
MyCompanyAuthenticationOptions options = new MyCompanyAuthenticationOptions
{
ClientId = clientId,
ClientSecret = clientSecret
};
return app.UseMyCompanyAuthentication(options);
}
}
My custom implementation of AuthenticationHandler<T>.AuthenticateCoreAsync() returns an AuthenticationTicket with the following claims and properties.
Claims
GivenName = My First Name
FamilyName = My Last Name
Name = My Full Name
Email = My Email Address
Properties
.redirect = /umbraco/
Dont have any code ready at hand but from past experiences, using Facebook OAuth, you will have to wire in your own logic to basically either or both, convert you OAuth object (user) into an umbraco one.
When we done it previously the first time a user does it (checking by email), it creates a new user then every subsequent login get the umbraco user by their email and log them in in code. This was the same for both backend users and front end members.
So after much wheel spinning, I finally figured it out. The resulting ClaimsIdentity didn't contain a NameIdentifier claim. I had my OAuth middleware include that claim using the email address as the value and it started working.
FYI, if you're looking to autolink external and local accounts upon external login, here's a really good example that worked for me.

Use SignalR to notify connected clients of new login to Web.API

I have an exiting set of APIs developed with ASP.NET Web API 2. Currently users need to authenticate before using most of the APIs (a few are public). Here is my scaled down OAuthProvider.
public class MyOAuthProvider : OAuthAuthorizationServerProvider
{
// Validation of user name and password takes place here
// Code not show for brevity
// If user is validated issue then issue them a JWT token
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, "JWT");
var ticket = new AuthenticationTicket(oAuthIdentity, null);
context.Validated(ticket);
}
In separate application I have experimented with SignalR and have a Hub class that takes the connectionId and userName of a user and stores it to an in-memory Dictionary named connectionDictionary.
public class ChatHub : Hub
{
public static Dictionary<string,string> connectionDictionary = new Dictionary<string, string>();
public override Task OnConnected()
{
connectionDictionary.Add(Context.ConnectionId, Context.User.Identity.Name);
return base.OnConnected();
}
}
How do I go about joining these together so that when a user authenticates with the API, their connectionId and userName are stored in the Dictionary? The idea being that once a user has logged in I will notify all other connected clients.

Resources