I have created a custom Authorize attribute where I use the Office Graph to get AAD groups the current user is member of, and based on those I reject or authorize the user. I want to save the groups, because the call to Office Graph takes some performance. What would be the correct way to save that kind of data? I can see some people saves it to a SQL server, but then I would need to ensure cleanup etc.
Also I can see in some threads the session state is stated to be a bad choice due to concurrency. So the question is what options do you have to store this kind of information?
All suggestions are welcome.
If you were only using the group_id info, there is no need to use Office Graph and store it at all. We can enable Azure AD issue the groups claims by change the manifest of Azure AD like below:(refer this code sample)
"groupMembershipClaims": "All",
And if you are also using other info about groups, you can store these info into claims. Here is a code sample that add the name of groups into claims for your reference:
AuthorizationCodeReceived = async context =>
{
ClientCredential credential = new ClientCredential(ConfigHelper.ClientId, ConfigHelper.AppKey);
string userObjectId = context.AuthenticationTicket.Identity.FindFirst(Globals.ObjectIdClaimType).Value;
AuthenticationContext authContext = new AuthenticationContext(ConfigHelper.Authority, new TokenDbCache(userObjectId));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.Code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, ConfigHelper.GraphResourceId);
ActiveDirectoryClient graphClient = new ActiveDirectoryClient(new Uri(ConfigHelper.GraphServiceRoot),
async () => { return await Task.FromResult(result.AccessToken); }
);
try
{
foreach (var groupClaim in context.AuthenticationTicket.Identity.FindAll("groups"))
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri($"https://graph.windows.net/adfei.onmicrosoft.com/groups/{groupClaim.Value}?api-version=1.6"),
Method = HttpMethod.Get,
};
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
using (HttpClient httpClient = new HttpClient())
{
HttpResponseMessage httpResponse = httpClient.SendAsync(request).Result;
var retJSON = httpResponse.Content.ReadAsStringAsync().Result;
var dict = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(retJSON);
((ClaimsIdentity)context.AuthenticationTicket.Identity).AddClaim(new Claim("groupName", dict["displayName"].ToString()));
}
}
}
catch (Exception ex)
{
}
},
Then we can these info from controller using the code below:
ClaimsPrincipal.Current.FindAll("groupName")
Related
I'm trying to use my own MSAL code to work together. Developed with .NET Core 5 MVC.
I have similar problem as I found in below link. But I just don't know how to make it work with the proposed answer. Or in other words, I'm still confuse how this integration is done.
[It is mandatory to use the login component in order to use the other components]It is mandatory to use the login component in order to use the other components
[Quickstart for MSAL JS]https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/main/samples/examples/simple-provider.html
I also have read following article too:
[Simple Provider Example]https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/main/samples/examples/simple-provider.html
[A lap around microsoft graph toolkit day 7]https://developer.microsoft.com/en-us/office/blogs/a-lap-around-microsoft-graph-toolkit-day-7-microsoft-graph-toolkit-providers/
is there someone can pointing to me more details explanation about how to archive this.
Can someone explains further below response further. How to do it. Where should I place the code and how to return AccessToken to SimpleProvider?
Edited:
Update my question to be more precise to what I want besides on top of the question. Below is the code I used in Startup.cs to automatically trigger pop up screen when user using the web app. When using the sample provided, it is always cannot get access token received or userid data. Question 2: How to save or store token received in memory or cache or cookies for later use by ProxyController and its classes.
//Sign in link under _layouts.aspx
<a class="nav-link" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
// Use OpenId authentication in Startup.cs
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
// Specify this is a web app and needs auth code flow
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.Prompt = "select_account";
options.Events.OnTokenValidated = async context =>
{
var tokenAcquisition = context.HttpContext.RequestServices
.GetRequiredService<ITokenAcquisition>();
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(async (request) =>
{
var token = await tokenAcquisition
.GetAccessTokenForUserAsync(GraphConstants.Scopes, user: context.Principal);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
})
);
// Get user information from Graph
try
{
var user = await graphClient.Me.Request()
.Select(u => new
{
u.DisplayName,
u.Mail,
u.UserPrincipalName,
u.MailboxSettings
})
.GetAsync();
context.Principal.AddUserGraphInfo(user);
}
catch (ServiceException)
{
}
// Get the user's photo
// If the user doesn't have a photo, this throws
try
{
var photo = await graphClient.Me
.Photos["48x48"]
.Content
.Request()
.GetAsync();
context.Principal.AddUserGraphPhoto(photo);
}
catch (ServiceException ex)
{
if (ex.IsMatch("ErrorItemNotFound") ||
ex.IsMatch("ConsumerPhotoIsNotSupported"))
{
context.Principal.AddUserGraphPhoto(null);
}
}
};
options.Events.OnAuthenticationFailed = context =>
{
var error = WebUtility.UrlEncode(context.Exception.Message);
context.Response
.Redirect($"/Home/ErrorWithMessage?message=Authentication+error&debug={error}");
context.HandleResponse();
return Task.FromResult(0);
};
options.Events.OnRemoteFailure = context =>
{
if (context.Failure is OpenIdConnectProtocolException)
{
var error = WebUtility.UrlEncode(context.Failure.Message);
context.Response
.Redirect($"/Home/ErrorWithMessage?message=Sign+in+error&debug={error}");
context.HandleResponse();
}
return Task.FromResult(0);
};
})
// Add ability to call web API (Graph)
// and get access tokens
.EnableTokenAcquisitionToCallDownstreamApi(options =>
{
Configuration.Bind("AzureAd", options);
}, GraphConstants.Scopes)
// Add a GraphServiceClient via dependency injection
.AddMicrosoftGraph(options =>
{
options.Scopes = string.Join(' ', GraphConstants.Scopes);
})
// Use in-memory token cache
// See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization
.AddInMemoryTokenCaches();
Since you are using MVC, I recommend using the ProxyProvider over the Simple Provider.
SimpleProvider - useful when you have existing authentication on the client side (such as Msal.js)
ProxyProvider - useful when you are authenticating on the backend and all graph calls are proxied from the client to your backend.
This .NET core MVC sample might help - it is using the ProxyProvider with the components
Finally, I have discovered how to do my last mile bridging for these two technology.
Following are the lines of the code that I have made the changes. Since I'm using new development method as oppose by MSAL.NET, a lot of implementation has been simplified, so many of examples or article out there, may not really able to use it directly.
Besides using links shared by #Nikola and me above, you also can try to use below
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/
to consolidate to become your very own solution. Below are the changes I have made to make it worked.
Change in Startup.cs class
// Add application services. services.AddSingleton<IGraphAuthProvider, GraphAuthProvider>(); //services.AddSingleton<IGraphServiceClientFactory, GraphServiceClientFactory>();
Change in ProxyController.cs class
private readonly GraphServiceClient _graphClient;
public ProxyController(IWebHostEnvironment hostingEnvironment, GraphServiceClient graphclient)
{
_env = hostingEnvironment;
//_graphServiceClientFactory = graphServiceClientFactory;
_graphClient = graphclient;
}
Change in ProcessRequestAsync method under ProxyController.cs
//var graphClient = _graphServiceClientFactory.GetAuthenticatedGraphClient((ClaimsIdentity)User.Identity);
var qs = HttpContext.Request.QueryString;
var url = $"{GetBaseUrlWithoutVersion(_graphClient)}/{all}{qs.ToUriComponent()}";
var request = new BaseRequest(url, _graphClient, null)
{
Method = method,
ContentType = HttpContext.Request.ContentType,
};
I was testing around with Google's oauth and was trying out different scopes.
However, I then reduced my scope request to just this : "https://www.googleapis.com/auth/userinfo.email"
The following is more in dotnetcore
Dictionary<string, string> queries = new Dictionary<string, string>();
queries.Add("scope", "https://www.googleapis.com/auth/userinfo.email");
queries.Add("access_type", "offline");
queries.Add("include_granted_scopes" ,"true");
queries.Add("response_type", "code");
queries.Add("state", "state");
queries.Add("redirect_uri", "http://localhost:5000/api/authenticate/googauth");
queries.Add("client_id", _clientId);
queries.Add("prompt", "consent");
UriBuilder builder = new UriBuilder();
builder.Host = "accounts.google.com";
builder.Scheme = "https";
builder.Path = "o/oauth2/v2/auth";
//builder.Query = ""
foreach (var query in queries)
{
if (string.IsNullOrEmpty(builder.Query))
{
builder.Query += $"{query.Key}={query.Value}";
}
else
{
builder.Query += $"&{query.Key}={query.Value}";
}
}
var redirectUri = builder.Uri.ToString();
return Redirect(redirectUri);
From the returned code, I then retrieved the access token etc.
Dictionary<string, string> values = new Dictionary<string, string>();
values.Add("code", code);
values.Add("client_id", _clientId);
values.Add("client_secret",_clientSecret);
values.Add("redirect_uri", "http://localhost:5000/api/authenticate/googauth");
values.Add("grant_type", "authorization_code");
var client = new HttpClient();
var result = await client.PostAsync("https://oauth2.googleapis.com/token", new FormUrlEncodedContent(values));
var content = await result.Content.ReadAsStringAsync();
var convertedContent = JsonSerializer.Deserialize<GoogleAccesstoken>(content);
However, I seem to get more than what I asked for. I get this in the returned scopes :
openid https://www.googleapis.com/auth/user.gender.read https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/user.birthday.read
I've tried using incognito, and different browsers and they all return the same thing (thinking that it may have been a cache issue).
Is anyone able to help me on this?
Thanks.
Enables applications to use incremental authorization to request access to additional scopes in context. If you set this parameter's value to true and the authorization request is granted, then the new access token will also cover any scopes to which the user previously granted the application access. See the incremental authorization section for examples.
extract from google documentation: https://developers.google.com/identity/protocols/oauth2/web-server
Basically means that the user has previously granted you the other scopes. Could have been through a login screen or something where you have used the same clientId
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 a set of Microsoft Teams that I'm unable to add a Microsoft Planner tab to. When I try and add the Planner I get the dialog and put in the Planner name and click Create and it gives back a Create Plan Failed message. No other information is returned.
This happens doesn't happen in all Microsoft Team, ones that are created normally in the teams app work fine, but ones that I create through the Microsoft Graph have this problem. Here is the code that I'm using to create the team.
public async Task<string> CreateTeam(string title, ClaimsPrincipal user)
{
var userId = user.Claims.First(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var body = $"{{\"displayName\":\"{title}\",\"groupTypes\":[\"Unified\"],\"mailEnabled\":true,\"mailNickname\":\"{title.Replace(" ", "")}\", \"securityEnabled\":false, \"visibility\":\"Private\" }}";
var res = await GraphClient.QueryGraphAsyncPost($"/groups", body, user);
var result = await res.Content.ReadAsStringAsync();
var group = JsonConvert.DeserializeObject<FieldInfoBucket>(result);
var id = group.Id;
res = await GraphClient.QueryGraphAsync($"/groups/{id}/owners", user);
result = await res.Content.ReadAsStringAsync();
body = $"{{\"#odata.id\": \"https://graph.microsoft.com/beta/users/{userId}\"}}";
res = await GraphClient.QueryGraphAsyncPost($"/groups/{id}/owners/$ref", body, user);
// ReSharper disable once RedundantAssignment
result = await res.Content.ReadAsStringAsync();
body =
$"{{\"memberSettings\":{{\"allowCreateUpdateChannels\":true, \"allowDeleteChannels\":true, \"allowAddRemoveApps\":true, \"allowCreateUpdateRemoveTabs\":true, \"allowCreateUpdateRemoveConnectors\":true}}, \"guestSettings\":{{\"allowCreateUpdateChannels\":false, \"allowDeleteChannels\":false}}, \"messageSettings\":{{\"allowUserEditMessages\":true, \"allowUserDeleteMessages\":true, \"allowOwnerDeleteMessages\":true, \"allowTeamMentions\":true, \"allowChannelMentions\":true}},\"funSettings\":{{\"allowGiphy\":true, \"giphyContentRating\":\"strict\",\"allowStickersAndMemes\":true,\"allowCustomMemes\":true}} }}";
res = await GraphClient.QueryGraphAsyncPut($"/groups/{id}/team", body, user);
// ReSharper disable once RedundantAssignment
result = await res.Content.ReadAsStringAsync();
return id;
}
The Graph client above simply issues Get/Post/Put commands against the graph.microsoft.com/beta endpoints and adds the appropriate Bearer token.
Planner is getting confused that its being asked by a user who's not a member of the team. If we add current logged in user (owner) explicitly using /AddMember api then it's workign fine. We are working on the fix.
Is there a possibility to configure OAuth2 AssertionFlow with Facebook in Thinktecture Identity Server v3?
There was a post on leastprivilege.com about implementing AssertionFlow for Microsoft OAuth and AuthorizationServer but I need to integrate with Facebook and, furthermore, AuthorizationServer is marked as deprecated and it's not maintained anymore.
In response to #NathanAldenSr's comment, I publish some code of my working solution.
Server side - custom validator:
public class FacebookCustomGrantValidator: ICustomGrantValidator
{
private readonly IUserService userService;
private const string _FACEBOOK_PROVIDER_NAME = "facebook";
// ...
async Task<CustomGrantValidationResult> ICustomGrantValidator.ValidateAsync(ValidatedTokenRequest request)
{
// check assetion type (you can have more than one in your app)
if (request.GrantType != "assertion_fb")
return await Task.FromResult<CustomGrantValidationResult>(null);
// I assume that fb access token has been sent as a response form value (with 'assertion' key)
var fbAccessToken = request.Raw.Get("assertion");
if (string.IsNullOrWhiteSpace(assertion))
return await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult
{
ErrorMessage = "Missing assertion."
});
AuthenticateResult authebticationResult = null;
// if fb access token is invalid you won't be able to create Facebook client
var client = new Facebook.FacebookClient(fbAccessToken);
dynamic response = client.Get("me", new { fields = "email, first_name, last_name" });
// create idsrv identity for the user
authebticationResult = await userService.AuthenticateExternalAsync(new ExternalIdentity()
{
Provider = _FACEBOOK_PROVIDER_NAME,
ProviderId = response.id,
Claims = new List<Claim>
{
new Claim("Email", response.email),
new Claim("FirstName", response.first_name),
new Claim("LastName", response.last_name)
// ... and so on...
}
},
new SignInMessage());
return new CustomGrantValidationResult
{
Principal = authebticationResult.User
};
}
}
You can easily test it with OAuth2Client that is also provided by Thinktecture (in Thinktexture.IdentityModel Client Library nuget package).
string fbAccessToken = "facebook_access_token_you_aquired_while_logging_in";
string assertionType = "assertion_fb";
var client = new OAuth2Client(
new Uri("your_auth_server_url"),
"idsrv_client_id",
"idsrv_client_secret");
string idsrvAccessToken = client.RequestAssertionAsync(assetionType, fbAccessToken,).Result;
IdentityServer v3 also supports assertion flow. The samples wiki has two samples on that (called "Custom Grants):
https://github.com/thinktecture/Thinktecture.IdentityServer.v3.Samples/tree/master/source