ASP.NET MVC 5 Dynamic Azure AD connectivity - asp.net-mvc

I am currently building a SaaS - application with ASP.NET MVC 5 as the core platform. One of the requirements is to allow tenants to login with their Azure AD Accounts.
Here is the background story:
It's a multi tenant application where each customer/tenant can have multiple users. Tenants share the same instance of the application, so they will ultimately all navigate to the same URL (myapplication.com). We allow users to login with their organizational accounts (Azure AD, Office 365). Since all tenants go to the same URL, we cannot determine upfront which client ID/secret to use as this will only work for just one client. By using subdomains, we can identify the client (e.g. client1.ourapplication.com), so we can retrieve the Azure AD data from the database.The next step would be to use pass this data to the OpenID Authentication Options for each request that comes in, instead of 'hard coding' it in compile time during the startup of the application.
The issue that I'm currently facing is how I can get rid of the hard coded ClientId and AppKeys in the Web.Config (or database, as is in this case) and dynamically change these values according to the user that is logging in. In other words, I need to create the Azure AD login provider on the fly for every login request.
In the full code sample below, I somehow need to be able to set this line app.UseOpenIdConnectAuthentication(...) during runtime and not during the startup of the application with the updated clientID and appKey of the tenant in question.
public void ConfigureAuth(IAppBuilder app)
{
string clientId = string.Empty;
string appKey = string.Empty;
ConfigurationWorker worker = new ConfigurationWorker(DependencyResolver.Current.GetService<IUnitOfWork>());
IEnumerable<Config> azureAdConfiguration = worker.GetConfiguration("AzureId", "AzurePassword").Result;
if (azureAdConfiguration != null && azureAdConfiguration.Count() == 2)
{
clientId = azureAdConfiguration.First(x => x.Key == "AzureId").Value;
appKey = azureAdConfiguration.First(x => x.Key == "AzurePassword").Value;
}
string graphResourceID = "https://graph.windows.net";
string Authority = "https://login.microsoftonline.com/common/";
app.UseOpenIdConnectAuthentication(this.GetAzureADLoginProvider(clientId, appKey, Authority, graphResourceID));
}
private OpenIdConnectAuthenticationOptions GetAzureADLoginProvider(string clientId, string appKey, string Authority, string graphResourceID)
{
return new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
AuthenticationMode = AuthenticationMode.Passive,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.microsoftonline.com/{0}", tenantID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
graphResourceID);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
// This ensures that the address used for sign in and sign out is picked up dynamically from the request
// this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
// Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
// we use this notification for injecting our custom logic
SecurityTokenValidated = (context) =>
{
// Once user has logged in, create and replace existing claims identity
ClaimsIdentity newClaimsIdentity = ClaimsHelper.Create(context.AuthenticationTicket.Identity, (x) => Singleton.Instance().Logger.LogInformation(x));
// Ultimately add tenant ID from azure to claims identity
Claim tenantIdClaim = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid");
newClaimsIdentity.AddClaim(tenantIdClaim);
// Set context's user to the new claim identity
context.AuthenticationTicket = new AuthenticationTicket(newClaimsIdentity, context.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.FromResult(0);
},
MessageReceived = (context) =>
{
return Task.FromResult(0);
},
SecurityTokenReceived = (context) =>
{
return Task.FromResult(0);
}
}
});
}
This is the AccountController's action that will be executed if the user selects to login with Azure AD:
[AllowAnonymous]
public void SignInAzure()
{
// Send an OpenID Connect sign-in request.
if (!Request.IsAuthenticated)
{
this.AuthenticationManager.Challenge(
new AuthenticationProperties { RedirectUri = Url.Action("AzureADCallback", new { returnUrl = "~/" }) },
OpenIdConnectAuthenticationDefaults.AuthenticationType
);
}
}
Can I somehow set the clientID and AppKey in this controller? Because at this point I'm able to determine the tenant (using the subdomain), which allows me to query the correct values (in the database).
Something like this pseudocode would be awesome to have:
this.AuthenticationManager.OpenIdConnectAuthenticationOptions.ClientID= NEWID;
this.AuthenticationManager.OpenIdConnectAuthenticationOptions.AppKey= NEWAPPKEY;
Or equally as well create the OpenIdConnectAuthenticationOptions object on the fly per request.
Is there any possibility to achieve this?
Update:
As a workaround for this issue, I currently use this approach, as suggested by this solution:
[AllowAnonymous]
public async Task<ActionResult> SignInAzure()
{
// Resolve tenant
string currentTenant = this.HttpContext.Request?.RequestContext?.RouteData?.Values["tenant"]?.ToString();
IEnumerable<Config> configKeys = await this.ConfigurationWorker.GetConfiguration(currentTenant, ConfigurationKeys.AzureId, ConfigurationKeys.AzurePassword);
string clientId = string.Empty;
string appKey = string.Empty;
if (configKeys != null && configKeys.Count() == 2)
{
clientId = configKeys.First(x => x.Key == ConfigurationKeys.AzureId).Value;
appKey = configKeys.First(x => x.Key == ConfigurationKeys.AzurePassword).Value;
}
string stateMarker = Guid.NewGuid().ToString();
string returnUrl = this.Request.Url.GetLeftPart(UriPartial.Authority).ToString() + "/Account/AzureADCallback";
string authorizationRequest = String.Format(
"https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&client_id={0}&resource={1}&redirect_uri={2}&state={3}",
Uri.EscapeDataString(clientId),
Uri.EscapeDataString("https://graph.windows.net"),
Uri.EscapeDataString(returnUrl),
Uri.EscapeDataString(stateMarker)
);
authorizationRequest += String.Format("&prompt={0}", Uri.EscapeDataString("admin_consent"));
return new RedirectResult(authorizationRequest);
}
[AllowAnonymous]
public async Task<ActionResult> AzureADCallback(string code, string error, string error_description, string resource, string state)
{
// Resolve tenant
string currentTenant = this.HttpContext.Request?.RequestContext?.RouteData?.Values["tenant"]?.ToString();
IEnumerable<Config> configKeys = await this.ConfigurationWorker.GetConfiguration(ConfigurationKeys.AzureId, ConfigurationKeys.AzurePassword);
string clientId = string.Empty;
string appKey = string.Empty;
if (configKeys != null && configKeys.Count() == 2)
{
clientId = configKeys.First(x => x.Key == ConfigurationKeys.AzureId).Value;
appKey = configKeys.First(x => x.Key == ConfigurationKeys.AzurePassword).Value;
}
ClientCredential credential = new ClientCredential(clientId, appKey);
AuthenticationContext authContext = new AuthenticationContext("https://login.windows.net/common/");
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(Request.Url.GetLeftPart(UriPartial.Path)), credential);
var user = this.UserManager.FindByEmail(result.UserInfo.DisplayableId);
await this.SignInAsync(user, false);
return RedirectToAction("Index", "Home");
}
In a nutshell, the tenant is resolved from the URL (subdomains) after which the database is called to get the Azure ID and Secret. This is used to generate the URL for the user to login. After the user has logged in to Azure AD, an AuthenticationResult is generated and the user is signed in to the application.

Related

How to configure Identity Server with NSwag API using IIdentityServerBuilder's .AddApiAuthorization()?

I'd like to create an authentication/authorization flow of sorts using Identity Server to have a user authorize themselves in my Swagger API so that they may access endpoints marked with the [Authorize] attribute. This is the current flow I have:
I have Swagger set up with the NSwag middleware with the OAuth2 security scheme:
services.AddMvcCore().AddApiExplorer();
services.AddOpenApiDocument(settings =>
{
settings.Title = "MyProject Services";
settings.Version = "1.0";
settings.AddSecurity("oauth2", new NSwag.OpenApiSecurityScheme
{
Type = NSwag.OpenApiSecuritySchemeType.OAuth2,
Flow = NSwag.OpenApiOAuth2Flow.AccessCode,
AuthorizationUrl = "/connect/authorize",
TokenUrl = "/connect/token",
Scopes = new Dictionary<string, string>
{
{ "MyProjectServicesAPI", "API Access" }
}
});
settings.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("oauth2"));
});
And the OAuth2 client settings in Configure():
app.UseOpenApi();
app.UseSwaggerUi3(options =>
{
options.OAuth2Client = new NSwag.AspNetCore.OAuth2ClientSettings
{
ClientId = "MyProjectAPI",
ClientSecret = "mysecret",
UsePkceWithAuthorizationCodeGrant = true
};
});
After a user selects the scope and authorizes, they get redirected to my Identity Server Login Page I scaffolded and from there they can login. Once they put in their credentials and press, 'Login', they then get redirected back to the Swagger API. So far so good. Now this is where I start to have trouble cause I would like to later add policies so a user must have a specific claim to access an endpoint, but right now, I'm not able to see any of my user's claims in the JWT Bearer token that's in the request header when I access and endpoint. The only information I get about my user is in the 'sub' which is their GUID. I'd like to be able to get their username, email, and role(s) as well.
This is what I have setup for Identity Server so far (and where I'm currently stuck):
Under ConfigureServices():
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.IdentityResources = new IdentityResourceCollection
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "roles",
DisplayName = "roles",
UserClaims = new List<string> { JwtClaimTypes.Role }
},
new IdentityResource
{
Name = "basicInfo",
DisplayName = "basic info",
UserClaims = new List<string> {
JwtClaimTypes.PreferredUserName
}
}
};
options.Clients = new ClientCollection
{
new Client
{
ClientId = "MyProjectAPI",
ClientName = "My Project Services API",
ClientSecrets = { new Secret("mysecret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "https://localhost:44319/swagger/oauth2-redirect.html" },
PostLogoutRedirectUris = { "https://localhost:44319/Identity/Account/Logout" },
AllowedScopes = {
"basicInfo",
"roles",
"MyProjectServicesAPI",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
RequirePkce = true,
RequireConsent = false
}
};
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerJwt()
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true
};
});
And then in the pipeline:
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
I recently got this error that's being thrown from Identity Server's OidcConfigurationController:
'Can't determine the type for the client 'MyProject''
I'm putting the Authorization Code type for the AllowedGrantTypes in my client so I'm not quite sure why it's throwing that error.
Do I need to be adding the claims to the Bearer token myself? If I'm including the scopes, why aren't those claims showing up? Thank you in advance for any assistance.
EDIT #1: I did resolve the error I was receiving from the OidcConfigurationController. I will add the JWT Bearer token only shows the 'MyProjectServicesAPI" scope and nothing else. However, my oidc discovery doc shows all of them?
I think I was able to partially solve my problem. So I didn't have Identity Server's Profile Service set up to grab my user's ID so it could grab the identity claims.
ProfileService.cs:
private readonly UserManager<ApplicationUser> _userManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
// Add custom claims to access token.
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.IssuedClaims.AddRange(context.Subject.Claims);
var user = await _userManager.GetUserAsync(context.Subject);
var roles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.PreferredUserName, user.UserName),
};
foreach (var claim in claims)
{
context.IssuedClaims.Add(claim);
}
foreach (var role in roles)
{
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, role));
}
}
public async Task IsActiveAsync(IsActiveContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
context.IsActive = (user != null) && user.LockoutEnabled;
}
And then back in Startup.cs:
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
....
})
.AddProfileService<ProfileService>();
And that's it! Order does matter as I did have AddProfileService() before my AddApiAuthorization() and that didn't work. All of my scopes still aren't showing in my JWT token, so I will need to revisit that, even though the right claims are being pulled from those Identity resources.

Authorize with Controllers with AD Groups using OIDC

we have a web application which uses OIDC for single sign on to authenticate with
Azure AD. The single sign on works great, Users are able to sign in with their AD accounts. The token it returns also contains AD groups.
I would like to authorize my MVC controllers to only allow certain groups to use certain controllers.
How do I implement this? I can see the groups are being sent back in the token represented as GUIDs.
I have tried setting the role claims via RoleClaimType = "roles", but this doesn't work.
here is my code.
public void ConfigureAuth(IAppBuilder app)
{
FATContext db = new FATContext();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager(),
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
// Set claimsPrincipal's roles to the roles claim
RoleClaimType = "roles",
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.RedirectUri = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path);
context.ProtocolMessage.PostLogoutRedirectUri = new UrlHelper(HttpContext.Current.Request.RequestContext).Action("Index", "Home", null, HttpContext.Current.Request.Url.Scheme);
return Task.FromResult(0);
},
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
return authContext.AcquireTokenByAuthorizationCodeAsync(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
}
}
});
}
So for example, how can I get this to work with my controller such as
[AuthorizeUser(Roles = "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
public ActionResult Index()
{
return View();
}
or to check is a user is in a certain role i.e User.IsInRole("example ")
In Azure AD we can define application roles and assign the roles to the groups.
Now the users of this group will have a claim with the roles defined(example as shown below). Refer this link for the detailed documentation on how to create/manage roles for an application in Azure AD.
{
"roles": ["admin"]
}
If you are just trying to use groups-based authorization you can use RoleClaimType = "groups" and the below example codes in controller or in the action methods. For more details you have similar information here
[Authorize(Roles = "Admin")]
public class TestAdminAccessController : ApiController
{
public ActionResult Index()
{
if (this.User.Identity.IsAuthenticated)
{
if (User.IsInRole("Admin"))
{
// Your logic
}
else
{
// is NOT an admin. No permissions
}
}
else
{
// is NOT Authenticated;
}
}
}

Get custom claims in client application after login cookie issued using Identity Server 3

We are in the process of learning Identity Server with the eventual aim of migrating our existing authentication service to it. For company, logistical and compatibility reasons, we are sticking with IS 3. We're not quite ready to move over to Core.
There are two parts to my question:
1) I have modified the sample app, available here, that uses a custom login page so that the browser prompts the user for their X509Certificate2 (as a partial login). The user enters a password and the certificate is used to call another endpoint which returns user-specific data. At that point, we wish to create custom user claims based on the returned data and then issue the cookie.
This all works fine up until the client receives the cookie. I cannot seem to extract the custom claims added to AuthenticatedLogin's Claims object on the client application. The client is configured to access all scopes.
It seems like I'm missing something very basic. Am I doing something wrong here? Bear in mind, these are just meaningless claims for test purposes.
2) Would this be an acceptable approach to issue claims? We would then likely use the returned cookie in order to call a separate authorisation service, as our roles are quite complex.
I have implemented the custom user service, with PreAuthenticateAsync redirecting to the custom login page:
public override Task PreAuthenticateAsync(PreAuthenticationContext context)
{
var id = ctx.Request.Query.Get("signin");
context.AuthenticateResult = new AuthenticateResult("~/custom/login?id=" + id, (IEnumerable<Claim>)null);
return Task.FromResult(0);
}
The controller method which creates the claims and calls IssueLoginCookie :
[RequireHttps]
[Route("core/custom/login")]
[HttpPost]
public ActionResult Index(string id, string password)
{
var userData = GetUser(password);
var owinEnvironment = Request.GetOwinContext().Environment;
var authenticatedLogin = new AuthenticatedLogin
{
IdentityProvider = Constants.BuiltInIdentityProvider,
Name = userData.UserName,
Subject = userData.EmailAddress,
Claims = GetClaims(userData),
PersistentLogin = false
};
owinEnvironment.IssueLoginCookie(authenticatedLogin);
var msg = owinEnvironment.GetSignInMessage(id);
var returnUrl = msg.ReturnUrl;
owinEnvironment.RemovePartialLoginCookie();
return Redirect(returnUrl);
}
// add our CUSTOM claims
private List<Claim> GetClaims(CustomUser authenticatedUser)
{
List<Claim> claims = new List<Claim>();
claims.Add(new Claim("claim1", authenticatedUser.CustomClaim1));
claims.Add(new Claim("claim2", authenticatedUser.CustomClaim2));
claims.Add(new Claim("claim3", authenticatedUser.CustomClaim3));
claims.Add(new Claim("Claim4", authenticatedUser.CustomClaim4));
return claims;
}
The client controller method with Authorize decorator:
[Authorize]
public ActionResult About()
{
// "CustomClaim1", "CustomClaim2" etc are not there :(
return View((User as ClaimsPrincipal).Claims);
}
The registered in-memory scope:
var scope1 = new Scope
{
Enabled = true,
Name = "user",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("CustomClaim1", true),
new ScopeClaim("CustomClaim2", true),
new ScopeClaim("CustomClaim3", true),
new ScopeClaim("CustomClaim4", true),
},
IncludeAllClaimsForUser = true
};
And finally the client's Configuration:
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost/idprov/core",
ClientId = "mvc",
RedirectUri = "https://localhost/dummyclient/About",
ResponseType = "id_token",
ClientSecret = "secret",
Scope = "openid partyuser",
SignInAsAuthenticationType = "Cookies",
});
}
Hi Try adding scope in your client like
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost/idprov/core",
ClientId = "mvc",
RedirectUri = "https://localhost/dummyclient/About",
ResponseType = "id_token",
ClientSecret = "secret",
Scope = "openid partyuser CustomClaim1 CustomClaim2",
SignInAsAuthenticationType = "Cookies",
});

Redirect other then Home/Index using OpenIdConnectAuthentication and Identity server after login

I'm trying to Redirect user to Dashboard but it always redirect it to Home/Index that is because I've set RedirectUri to http://localhost:35641/ in Identity Server Options. But that is true in case of application landing page after login it needs to redirect o dashboard. I can write custom logic in Index's Action Result but I want to avoid it.
MVC web Startup method
public void Configuration(IAppBuilder app)
{
// Implicit mvc owin
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ApplicationConstants.ClientIdNucleusMvcApp,
Authority = ApplicationConstants.UrlBaseAuth,
RedirectUri = ApplicationConstants.UrlBaseWeb,
PostLogoutRedirectUri = ApplicationConstants.UrlBaseWeb,
ResponseType = "id_token token",
Scope = string.Format("openid email {0}", ApplicationScopes.MvcApp),
SignInAsAuthenticationType = "Cookies",
// sample how to access token on form (when adding the token response type)
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
// Adding access token in claims
var accessToken = n.ProtocolMessage.AccessToken;
if (!string.IsNullOrEmpty(accessToken))
{
n.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", accessToken));
}
// Adding identity token in claims
var identityToken = n.ProtocolMessage.IdToken;
if (!string.IsNullOrEmpty(identityToken))
{
n.AuthenticationTicket.Identity.AddClaim(new Claim("identity_token", identityToken));
}
},
RedirectToIdentityProvider = async n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idToken = n.OwinContext.Authentication.User.FindFirst("identity_token");
n.ProtocolMessage.IdTokenHint = idToken == null ? null : idToken.Value;
n.ProtocolMessage.PostLogoutRedirectUri = ApplicationConstants.UrlBaseWeb;
}
}
}
});
}
Here is my Client on Identity Server
new Client
{
Enabled = true,
ClientName = ApplicationConstants.ClientNameNucleusMvcApp,
ClientId = ApplicationConstants.ClientIdNucleusMvcApp,
ClientSecrets = new List<ClientSecret>
{
new ClientSecret(ApplicationConstants.ClientSecretNucleusMvcApp.Sha256())
},
Flow = Flows.Implicit,
RequireConsent = false,
AccessTokenType = AccessTokenType.Reference,
IdentityTokenLifetime = 1800,
AccessTokenLifetime = 1800,
RedirectUris = new List<string>
{
// MVC form post sample
ApplicationConstants.UrlBaseWeb,
ApplicationConstants.UrlBaseWeb + "Dashboard/Index"
},
PostLogoutRedirectUris = new List<string>
{
ApplicationConstants.UrlBaseWeb
}
}
Help will be appreciated. Thanks
The RedirectUri you use for talking with your authority should not make a difference, that's just used for dispatching the token back to your application. After that there is an internal (==local to the app) redirect that is used for setting the session cookie and can go anywhere you want within the site. How do you trigger authentication? If you started from a protected action via [authorize], you should always land back in there in the end. If you are using explicit sign in code like if
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
you can always specify whatever desired landing route you want in RedirectUri. I know, it is fantastically confusing that the property driving this internal redirect has the exact same name as the protocol counterpart - the only excuse we have is that the AuthenticationProperties class already existed when the new claims based middleware was introduced, and calling the actual OAuth/OIDC redirect_uri with the underscore didn't fly with the .NET community. HTH

Azure ActiveDirectory Graph API GraphClient not returning AD Groups

I want to retrieve a User's Group information from Azure AD.
Using the following Graph API packages to achieve this
Microsoft.Azure.ActiveDirectory.GraphClient
Microsoft.IdentityModel.Clients.ActiveDirectory 2.13.112191810
I am able to successfully retrieve Users information from the Azure Graph API.
But when I run this method to retrieve a User's groups, Fiddler shows a successful HTTP 200 response with JSON fragment containing group information however the method itself does not return with the IEnumerable.
IEnumerable<string> groups = user.GetMemberGroupsAsync(false).Result.ToList();
The code doesn't seem to return from this async request.
The resulting experience is blank page while the authentication pipeline gets stuck.
Full code
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated == true &&
_authorizationService.IdentityRegistered(incomingPrincipal.Identity.Name))
{
return base.Authenticate(resourceName, incomingPrincipal);
}
_authorizationService.AddClaimsToIdentity(((ClaimsIdentity) incomingPrincipal.Identity));
Claim tenantClaim = incomingPrincipal.FindFirst(TenantIdClaim);
if (tenantClaim == null)
{
throw new NotSupportedException("Tenant claim not available, role authentication is not supported");
}
string tenantId = tenantClaim.Value;
string authority = String.Format(CultureInfo.InvariantCulture, _aadInstance, _tenant);
Uri servicePointUri = new Uri("https://graph.windows.net");
ClientCredential clientCredential = new ClientCredential(_clientId, _password);
AuthenticationContext authContext = new AuthenticationContext(authority, true);
AuthenticationResult result = authContext.AcquireToken(servicePointUri.ToString(), clientCredential);
Token = result.AccessToken;
ActiveDirectoryClient activeDirectoryClient =
new ActiveDirectoryClient(new Uri(servicePointUri, tenantId),
async () => await AcquireTokenAsync());
IUser user = activeDirectoryClient
.Users
.Where(x => x.UserPrincipalName.Equals(incomingPrincipal.Identity.Name))
.ExecuteAsync()
.Result
.CurrentPage
.ToList()
.FirstOrDefault();
if (user == null)
{
throw new NotSupportedException("Unknown User.");
}
IEnumerable<string> groups = user.GetMemberGroupsAsync(false).Result.ToList();
return incomingPrincipal;
}
I have the same problem. My code is working after changing it according to documentation
https://github.com/AzureADSamples/ConsoleApp-GraphAPI-DotNet
IUserFetcher retrievedUserFetcher = (User) user;
IPagedCollection<IDirectoryObject> pagedCollection = retrievedUserFetcher.MemberOf.ExecuteAsync().Result;
do {
List<IDirectoryObject> directoryObjects = pagedCollection.CurrentPage.ToList();
foreach (IDirectoryObject directoryObject in directoryObjects) {
if (directoryObject is Group) {
Group group = directoryObject as Group;
((ClaimsIdentity)incomingPrincipal.Identity).AddClaim(
new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String, "GRAPH"));
}
}
pagedCollection = pagedCollection.GetNextPageAsync().Result;
} while (pagedCollection != null && pagedCollection.MorePagesAvailable);
IEnumerable, string groups = user.GetMemberGroupsAsync(false).Result.ToList() doesn't work since the result is not of type IEnumerable, string.
IEnumerable<string> groups = await user.GetMemberGroupsAsync(false);
Above code would return the correct type.

Resources