Azure AD B2C with WebApi2- calling GraphAPI after authentication - c#

I have a WebApi2 app which servers as api for my app frontend. Now i want to use AD B2C to manage my users - let's say I want to differentiate them by their roles (admin or customer) and for that i created two b2c users groups accordingly. When user logs in i want to display different things for users with different roles (groups).
I'm using this example to setup Startup.Auth.cs in my WebApi2 project:
var tvps = new TokenValidationParameters
{
ValidAudience = clientId,
AuthenticationType = signUpSignInPolicy,
};
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(String.Format(aadInstance, tenant, defaultPolicy))),
});
From what I have read b2c doesn't return user's grups in claims for now. Some people suggested I need to call GraphApi after obtaining token to fetch these groups and add them to user's claims:
private static async Task<string> GetGroups(string token, string userId)
{
using (var client = new HttpClient())
{
var requestUrl = $"https://graph.microsoft.com/v1.0/users/{userId}/memberOf?$select=displayName";
var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
}
This is where I've stuck. How can I inject my code to get token for calling graph? I've messed with OAuthBearerAuthenticationOptions.Provider:
Provider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = (context) =>
{
// var token = ??
// var userId = <get from context's claims>
// var groups = GetGroups(token, userId);
// <add to claims>
return Task.CompletedTask;
}
},
...but I don't know how to get to token. Maybe that's wrong from the start and I need another approach?

Customer's token cannot be used to call AADGraph/MSGraph Apis. To get token to call graph apis in an automated way, we need app-only access. We need to configre an app in the tenant, the crendetial of which are used to get a token. That token can then be used to call memberOF Api (or any other api which does or require user information to be there)
Here is the sample and explaination of how to call AAD Graph apis in a B2C dependent service.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-graph-dotnet

Related

ASP.NET Identity - Get Saved third-party access tokens

I have an app that will operate almost entirely on Spotify OAuth, that will have features to alter the playback of your music.
I'm able to get Spotify OAuth working perfectly such that I can log into my app, but after I've logged in, I need access to the current user's spotify access_token so that I can forward it to my spotify requests.
I followed this guide from ms to try to save the tokens: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/?view=aspnetcore-6.0&tabs=visual-studio
And I have tried all these ways to then save that token into the HttpContext such that I can access it:
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
var spotifyAccessToken = tokens.FirstOrDefault(x => x.Name == "access_token").Value;
tokens.Add(new AuthenticationToken()
{
Name = "SpofityAccessToken",
Value = spotifyAccessToken
});
//store all the tokens as directed by MS
ctx.Properties.StoreTokens(tokens);
//store the properties into the HttpContext in 2 different ways
ctx.HttpContext.Items["Properties"] = ctx.Properties;
ctx.HttpContext.Features.Set(ctx.Properties);
//try adding a claim to the user
ctx.Identity.AddClaims(new[] { new Claim("SpotifyAccessToken", spotifyAccessToken) });
return Task.CompletedTask;
};
The problem I'm having is how do I then get this token out? all of these methods are not working:
[HttpGet]
public async Task Get()
{
await HttpContext.SignInAsync(User);
// try to re-run Authenticate, even though i'm already in an [Authorize] controller
var res = await HttpContext.AuthenticateAsync();
//props2 does not have the tokens i set
var props2 = res.Properties;
//props comes back null
var props = HttpContext.Features.Get<AuthenticationProperties>();
//claims has no SpotifyAccessToken claim
var claims = User.Claims.ToList();
var token = "hard-coded";
//here is where i need the token to then query spotify
var client = new SpotifyAPI.Web.SpotifyClient(token);
var res2 = await client.Player.GetCurrentPlayback();
}
I feel like I've tried everything, what am i doing wrong?
This is in a .NET 6 blazor wasm, .net core hosted app.
Also tried the solutions here to no avail Get AuthenticationProperties in current HttpRequest after HttpContext.SignInAsync
signInManager.UpdateExternalAuthenticationTokensAsync adds the the authentication tokens in [dbo].[AspNetUserTokens]
External login is where I call it:
// Sign in the user with this external login provider if the user already has a login.
var signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: true, bypassTwoFactor: true);
if (signInResult.Succeeded)
{
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(returnUrl);
}
Later on you can get it by using :
var token = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "access_token");
var expiresAtStr = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "expires_at");
If the token is stored in the Cookie then you can access the various tokens using:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string idToken = await HttpContext.GetTokenAsync("id_token");
string refreshToken = await HttpContext.GetTokenAsync("refresh_token");
string tokenType = await HttpContext.GetTokenAsync("token_type");
string accessTokenExpire = await HttpContext.GetTokenAsync("expires_at");
However, you can not store data in ctx.HttpContext and assume it will be persisted across requests. either you sign-in the user using the cookie middleware or you store the tokens in the UserSession object.
See this article on how to configure and store data in the session, that will be persisted across requests.
Session and state management in ASP.NET Core
If you configure it properly, then you can use it like:
HttpContext.Session.SetString("token", token.Trim());

Get accesstoken from azure in web with openidconnect

I have a desktop app where I authenticate users via Azure via AuthenticationContext.AcquireTokenAsync.
With the result from this method I can get the access-token, send it to my WCF and in my WCF use JwtSecurityToken / ConfigurationManager< OpenIdConnectConfiguration > to validate the token.
I've implemented login via Azure in a web app now by configuring it with app.UseOpenIdConnectAuthentication. So in my web app I dont explicitly call a method that returns a token. Rather I jack this in in asp.net's flow.
But now I want to fetch the token in a method and send it for validation similiarly how I did in my desktop app. I cannot find any token that the ConfigurationManager accepts however. I've looked in the regular HttpContext and Owincontext but no info that I find there is useful. Is the accesstoken stored anywhere where I can fetch it? Or do I have to do another request to get an accesstoken?
You should be getting access token as part of the response.
A simple way would be to look at the Authorization header. Look at code below -
HttpContext.Current.Request.Headers["Authorization"];
Also, I don't know what you mean by send the token for validation.
If you're trying to validate the token manually, here's a sample that does exactly that -
Manually validating a JWT access token in a web API
In the sample, specifically look at the Global.asax.cs
string jwtToken = null;
AuthenticationHeaderValue authHeader = request.Headers.Authorization;
if (authHeader != null)
{
jwtToken = authHeader.Parameter;
}
if (jwtToken == null)
{
HttpResponseMessage response = this.BuildResponseErrorMessage(HttpStatusCode.Unauthorized);
return response;
}
.........
.........
.........
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters = new TokenValidationParameters
{
// We accept both the App Id URI and the AppId of this service application
ValidAudiences = new[] { audience, clientId },
// Supports both the Azure AD V1 and V2 endpoint
ValidIssuers = new[] { issuer, $"{issuer}/v2.0" },
IssuerSigningKeys = signingKeys
};
try
{
// Validate token.
SecurityToken validatedToken = new JwtSecurityToken();
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters, out validatedToken);

How do I use OpenID and Microsoft Graph to enumerate Azure ActiveDirectory groups?

I have the following values:
OpenID App Key
OpenID Audience
OpenID Client ID
OpenID Login URL/Domain
Token Endpoint (https://login.windows.net/<tenant-id>/oauth2/token)
Resource URL (https://graph.windows.net)
How do I use these values to create a Microsoft Graph service client?
var graphClient = new GraphServiceClient(
// What goes here?
);
I need the client to enumerate AAD groups.
Based on your description, I assumed that you are using the AAD v1.0, for using the Microsoft Graph client SDK, you need to add Required permissions to the Microsoft Graph API with the application permissions or delegated permissions for your AAD application on Azure Portal. Differences between application permissions and delegated permissions, you could follow here.
For web application and use the user-based authentication flow, you could follow the samples below:
Calling the Azure AD Graph API in a web application
Microsoft Graph Snippets Sample for ASP.NET 4.6
Note: For your scenario, you need to combine the code in the above two samples. Or you could just create the AAD v2.0 application and just use the second sample.
For server to server scenario, you could just use ADAL to retrieve the access token to initialize your GraphServiceClient:
private static async Task<string> GetAccessTokenAsync()
{
string tenantId = "<tenantId>";
string clientId = "<clientId>";
string clientSecrets = "<clientSecrets>";
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult result = null;
var context = new AuthenticationContext(String.Format("https://login.windows.net/{0}", tenantId));
var authParam = new PlatformParameters(PromptBehavior.Never, null);
var result = await context.AcquireTokenAsync(
"https://graph.microsoft.com"
, new Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential(clientId, clientSecrets)
);
return result.AccessToken;
}
//initialize the GraphServiceClient instance
var graphClient = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
var token = await GetAccessTokenAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
}));

Swagger UI will return API data, but my Authorized calls return "permission denied"

So I believe my APIservice should be fine since I can return results through Swagger? I am calling from a WPF project. I launch the program and it asks me to login, then it continues and will tell me I don't have permission.
I'm super green to WebAPI2 and think I may just be constructing my call incorrectly. It does seem that I get a token back correctly from my site, the only issue is when I try to actually call on the API for data.
Here is my code:
public static string clientId = "{#Calling App Id}";
public static string commonAuthority = "https://login.windows.net/{#my Azure AD tenant}";
public static Uri returnUri = new Uri("http://MyDirectorySearcherApp");
const string ResourceUri = "https://{#Api App Service}.azurewebsites.net";
public static async Task<List<User>> LoadBands(IPlatformParameters parent)
{
AuthenticationResult authResult = null;
List<User> results = new List<User>();
try {
//get token or use refresh
AuthenticationContext authContext = new AuthenticationContext(commonAuthority);
if (authContext.TokenCache.ReadItems().Count() > 0)
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
authResult = await authContext.AcquireTokenAsync(ResourceUri, clientId, returnUri, parent);
} catch (Exception ee) {
throw ex;
}
using (var httpClient = new HttpClient()) {
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{ResourceUri}/api/Band/")) {
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
using (var response = await httpClient.SendAsync(request)) {
string responseData = await response.Content.ReadAsStringAsync();
//responseData always equals "You do not have permission to view this directory or page"
return results;
}
}
}
Edit: Maybe helpful to note I'm using a DataAPI that is called by a Rest API, the rest API is secured by Azure AD.
Edit: I'm calling from a Portable Class Library.
Edit: Well, I'm getting authenticated but it does not appear to make any difference. If I completely remove the Auth header I get the same result
It seems that the token is incorrect for the web API which protected by Azure AD. Please check the aud claim in the token which should match the Audience you config in the web API project. You can check the aud claim by parse the token from this site.
And if you still have the problem please share the code how you protect the web API.
Update
If you were using the Express mode like below, you need to acquire the access_token using the app which you associate with the web API.
If you were using the Advanced mode, we should also use the that app to acquire the token and the ResourceUri should matched the value you config in ALLOWED TOKEN AUDIENCES like below:

Register External Login Web API

I don't understand why their isn't a clear tutorial or guideline on this, so I hope my question can be answered here.
So, trying to register users from facebook or google, via the Web Api.
The problem is, at the RegisterExternal method, on this line:
var info = await Authentication.GetExternalLoginInfoAsync();
It returns null, and thus returning a BadRequest()
What I got so far:
In Startup.Auth.cs I've hadded the id's and the secrets, note that I have also tried using Microsoft.Owin.Security.Facebook
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions
{
AppId = "103596246642104",
AppSecret = "1c9c8f696e47bbc661702821c5a8ae75",
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, ClaimValueTypes.String, "Facebook"));
return Task.FromResult(0);
}
},
};
facebookOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookOptions);
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "328779658984-t9d67rh2nr681bahfusan0m5vuqeck13.apps.googleusercontent.com",
ClientSecret = "ZYcNHxBqH56Y0J2-tYowp9q0",
CallbackPath = new PathString("/api/Account/ManageInfo")
});
facebookOptions source: this post
That extra facebookOptions did not solve the problem.
I am able to retrieve an access_token from both Google and Facebook. I'm also able to Authenticate with this access_token to api/Account/UserInfo
GET http://localhost:4856/api/Account/UserInfo
in the header:
Authorization: Bearer R9BTVhI0...
Which returns:
{"Email":"firstname lastname","HasRegistered":false,"LoginProvider":"Facebook"}
One issue I notice their, is that it returns my name as Email, not the actual Email adress.
Now I want to register the external login with a new user for my database, which I make a POST call like this:
POST http://localhost:4856/api/Account/RegisterExternal
[header]
authorization: bearer 6xcJoutY...
Content-Type: application/json
[body]
{"Email":"...#hotmail.com"}
source: this post
Now this returns a BadRequest on this code snippit, inside RegisterExternal():
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
//AuthenticationManger?
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
In debugging, the ExternalLoginConfirmationViewModel does contain my email adress.
What am I doing wrong? Do I have to add something to the Startup.cs? Is there something more I have to do in the Startup.Auth.cs? Am I incorrectly calling RegisterExternal? In MVC it goes so smooth, why not in the Web API?
Aso looked at this answer from this question, But I didn't understand how to implement this.
This method is not really practical, since you are developing an API, that will most likely be used for apps, you best way is to handle the login with facebook by the API consumer, and let them send you an facebook auth token.
Basically I was trying to do this:
Create external login link for facebook.
Send user to that link that will bring them to facebook login page.
After login facebook will redirect to api.
User would be registered, but how does the app/website that is consuming the API know?
What you want to do is this:
API consumer creates their own method to login with facebook (for apps via SDK's)
API consumer will send an facebook token to the API to register/login.
API will check token with facebook graph endpoint.
When succeeded, API will return an bearer token for the API to make further authenticated requests.
So for you as an API developer, you would verify the token like so:
var verifyTokenEndPoint = string.Format("https://graph.facebook.com/debug_token?input_token={0}&access_token={1}", accessToken, appToken);
And then get the userId
var client = new HttpClient();
var uri = new Uri(verifyTokenEndPoint);
var response = await client.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
dynamic jObj = (JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);
string user_id = jObj["data"]["user_id"];
string app_id = jObj["data"]["app_id"];
}
Eventually you would create or find a user like so:
IdentityUser user = await _userManager.FindAsync(new UserLoginInfo(provider, verifiedAccessToken.user_id));
And then it's all up to you how to create an bearer token, if you follow the tutorial listed below, you could have this:
var tokenExpiration = TimeSpan.FromMinutes(30);
ClaimsIdentity identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userName));
identity.AddClaim(new Claim("role", "user"));
var props = new AuthenticationProperties()
{
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.Add(tokenExpiration),
};
var ticket = new AuthenticationTicket(identity, props);
var accessToken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket);
Source, with full tutorial here
I've also got the email via the SDK and send that along with the POST request, since I managed both the API and the consumer. Warning though: A facebook user might not want to give you an e-mail address.
Get e-mail after facebook login on Android and IOS

Categories