Multiple OpenIdConnectAuthentication-Middlewares for Multitenancy - asp.net-mvc

We are trying to use the marvelous IdentityServer for our product. Your application should be able to work with different tenants and each tenant might have their own identityproviders.
The IdentityServer part "could" (it works, but I'm not sure if this is super clever) be solved like this:
app.Map("/demotenant", (test) =>
{
test.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = Certificate.Load(),
Factory = factory,
RequireSsl = false,
AuthenticationOptions = new AuthenticationOptions
{
EnableLocalLogin = false,
IdentityProviders = ConfigureIdentityProviders,
},
});
});
app.Map("/demotenant2", (test) =>
{
test.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = Certificate.Load(),
Factory = factory,
RequireSsl = false,
AuthenticationOptions = new AuthenticationOptions
{
EnableLocalLogin = false,
IdentityProviders = ConfigureIdentityProviders,
},
});
});
Now I tried to use this from my webapplication. When I'm working on /demotenant it should use the /demotenant-identity-server etc.
app.Map("/demotenant", (test) =>
{
test.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "cookies",
});
test.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "cookies",
Authority = "http://localhost:63958/demotenant",
ClientId = "webapp",
RedirectUri = "http://localhost:57354/",
ResponseType = "id_token",
Scope = "openid",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async f =>
{
f.ProtocolMessage.AcrValues = "datasourceId:test";
},
},
});
});
app.Map("/demotenant2", (test) =>
{
test.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "cookies",
});
test.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "cookies",
Authority = "http://localhost:63958/demotenant2",
ClientId = "webapp",
RedirectUri = "http://localhost:57354/",
ResponseType = "id_token",
Scope = "openid",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async f =>
{
f.ProtocolMessage.AcrValues = "datasourceId:test";
}
},
});
});
Unfortunately it doesn't work or at least I can't trigger the authentication flow.
My "simple" sample just use the [Authorize] attribute, which magically redirects me to my IdentityServer.
So the question is:
- Is it possible to trigger one of the authroization based on a routing, if yes: How?

Kindly note that, you should not have to use the app.Map to route to the Identity providers [IDP] based on the tenant. In the IdentityServer, you will have to figure out the exact IDP based on the tenant. Then you can just call the challenge on that particular IDP.
Steps
Identify the tenant
Look-up the IDP for the tenant
Now, from the registered IDP's from the Owin Middlewares, you will have to invoke the challenge.
4.The end points / clientID & secrets are to be resolved from the identified tenant.
This enables a fully dynamic pipeline without tenant specific routes as in a Multi-tenant production app, we do not want to make a fresh deployment update for each new tenant that we bring on-board.
For Example, in case of a tenant opting for Social Identity providers, the loginmodel.Providers will contain all the login options like Facebook, Google, Twitter etc
The user can choose and click on login.
Code sample to resolve the endpoint uris based on a tenant from an OWIN Middleware
OnDemandEndpoints = async (clientid, EndpointUriTypes) =>
{
var endpointResolver = ServiceLocator.Resolve<IClientEndpointResolver>();
return await endpointResolver.ResolveEndpointUri(EndpointUriTypes, clientid);
},
There needs to be some context that can carry your tenant context across the middleware so that from the endpoint resolution to the clientId and ClientSecrets can be resolved dynamically.
Hope this is helpful for you

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.

Microsoft login with subdomains

Using this guide: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-asp-webapp i have added Microsoft Login to two projects.
The projects are placed as subdomains on the same domain and i would like for them to share login.
On the CookieAuthenticationOptions i have tried setting CookieDomain. This is what i have in my Startup.cs
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
CookieAuthenticationOptions options = new CookieAuthenticationOptions {
CookieName = "mytestcookie",
CookieDomain = ".azurewebsites.net",
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie
};
app.UseCookieAuthentication(options);
var ss1 = app.GetDefaultSignInAsAuthenticationType();
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile + " email",
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = OnAuthenticationFailed,
RedirectToIdentityProvider = notification => {
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication) {
if ((IsAjaxRequest(notification.Request) || IsApiRequest(notification.Request)) && notification.Response.StatusCode == (int)HttpStatusCode.Unauthorized) {
notification.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
notification.HandleResponse();
return Task.FromResult(0);
}
}
return Task.FromResult(0);
},
},
UseTokenLifetime = false
});
However this breaks something, resulting microsoft login redirecting me back and forth a couple of times.
According to: ASP.NET Core Sharing Identity Cookie across azure web apps on default domain (*.azurewebsites.net)
.azurewebsites.net is blacklisted.
Using my own domain fixed the issue.

Azure B2C authentication redirects locally, but not when hosted on Azure

I'm new to Azure AD B2C. I'm trying to set up azure B2C authentication for an MVC application.
The login works fine locally, but when it's not working on server.
The application is hosted on Azure AD.
I don't know if I missed something!! Can someone please help?
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// set the authentication type to the id of the policy
MetadataAddress = metaDataAddress,
AuthenticationType = policy,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = clientId,
//ClientSecret = clientSecret,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = AuthenticationFailed,
},
Scope = "openid",
ResponseType = "id_token",
// used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
SaveSigninToken = true
}
};
}
When it is deployed on to the server, after sign in, it is not returning to the application. Instead the page seems to blink and in between I can see something displayed as "As part of the authentication process the page is displayed several times, please click the button to continue"..
Startup.cs file seems alright to me.Couple things to cross check:
redirectUri matches with the website
Here is my signup and sign in action method:
public void SignIn()
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" }, Startup.SignInPolicyId);
}
}
public void SignUp()
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" }, Startup.SignUpPolicyId);
}
}
I have followed below github repo and it worked for me. Try this and see if it works
https://github.com/tjoudeh/Azure-Active-Directory-B2C/tree/master/AADB2C.WebClientMvc/Controllers
Reference:
http://bitoftech.net/2016/08/31/integrate-azure-ad-b2c-asp-net-mvc-web-app/
Please provide the detailed error with entire code base will try to reproduce at my end.

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

Resources