AddMicrosoftIdentityWebApp with two providers isn't setting IsAuthenticated - azure-ad-b2c

I'm trying to get a dual authentication approach working for my .NET6 website. For the front-end, I'm implementing Azure AD B2C, and for the back-end, Azure AD. Here's my code:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options => {
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.Instance = "Instance1";
options.TenantId = "TenantId1";
options.ClientId = "ClientId1";
options.ClientSecret = "ClientSecret1";
options.CallbackPath = "/signin-oidc/aadb2b";
options.Scope.Clear();
options.Scope.Add(OpenIdConnectScope.OpenId);
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
options.Scope.Add(OpenIdConnectScope.Email);
options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username",
ValidateIssuer = false
};
options.Events.OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Response.StatusCode == 401)
{
ctx.HandleResponse();
}
return Task.CompletedTask;
};
options.Events.OnAuthenticationFailed = ctx =>
{
ctx.HandleResponse();
ctx.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(ctx.Exception.Message));
return Task.CompletedTask;
};
}, options => {
options.Events.OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
{
// Syncs user and roles so they are available to the CMS
var synchronizingUserService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(claimsIdentity);
}
};
}, "AADB2B.OpenIdConnect", "AADB2B.Cookies");
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options => {
options.Instance = "Instance2";
options.Domain = "Domain2";
options.TenantId = "TenantId2";
options.ClientId = "ClientId2";
options.ClientSecret = "ClientSecret2";
options.SignUpSignInPolicyId = "USUIP";
options.ResetPasswordPolicyId = "RPP";
options.EditProfilePolicyId = "EPP";
options.CallbackPath = "/signin-oidc/aadb2c";
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles"
};
options.Events.OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Response.StatusCode == 401)
{
ctx.HandleResponse();
}
return Task.CompletedTask;
};
options.Events.OnAuthenticationFailed = ctx =>
{
ctx.HandleResponse();
ctx.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(ctx.Exception.Message));
return Task.CompletedTask;
};
}, options => {
options.Events.OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
{
// Syncs user and roles so they are available to the CMS
var synchronizingUserService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(claimsIdentity);
}
};
}, "AADB2C.OpenIdConnect", "AADB2C.Cookies");
// Added as an experiment, doesn't seem to help
services.AddAuthorization(options =>
options.DefaultPolicy =
new AuthorizationPolicyBuilder("AADB2B.OpenIdConnect")
.RequireAuthenticatedUser()
.Build());
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseNotFoundHandler();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseGetaCategories();
app.UseGetaCategoriesFind();
app.UseAnonymousId();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/LoginPath", async ctx => ctx.Response.Redirect("/")).RequireAuthorization(authorizeData: new AuthorizeAttribute { AuthenticationSchemes = "AADB2B.OpenIdConnect" });
endpoints.MapGet("/LogoutPath", async ctx => await MapLogout(ctx));
endpoints.MapControllerRoute(name: "Default", pattern: "{controller}/{action}/{id?}");
endpoints.MapControllers();
endpoints.MapRazorPages();
endpoints.MapContent();
});
}
public async Task MapLogout(HttpContext ctx)
{
await ctx.SignOutAsync("AADB2B.OpenIdConnect");
await ctx.SignOutAsync("AADB2B.Cookies");
ctx.Response.Redirect("/");
}
Controller.cs
[HttpGet]
[AllowAnonymous]
public IActionResult ExternalLogin(string scheme, string returnUrl)
{
return Challenge(new AuthenticationProperties { RedirectUri = string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl });
}
Controller is receiving a hyperlink with the QueryString scheme=AADB2B.OpenIdConnect and scheme=AADB2C.OpenIdConnect respectively.
Upon clicking the hyperlinks, the browser is properly redirected to the signin page for AAD B2C or AAD respectively, and then properly redirected back to the website. A breakpoint in the OnSignedIn event properly shows that the Principal.Identity is indeed a ClaimsIdentity, and IsAuthenticated is true. When arriving in the website, the cookies seem to exist:
However, after the page finishes loading, checking IHttpContextAccessor on subsequent pages shows that the HttpContext.User seems to be a brand-new one, and not the one that exists after the above authentication call.
I tried changing to this:
[HttpGet]
[AllowAnonymous]
public IActionResult ExternalLogin(string scheme, string returnUrl)
{
return Challenge(new AuthenticationProperties { RedirectUri = Url.Action("ExternalLoginCallback", new { scheme = scheme, returnUrl = returnUrl }) }, scheme);
}
[Authorize(AuthenticationSchemes = "AADB2B.OpenIdConnect,AADB2C.OpenIdConnect")]
public async Task<ActionResult> ExternalLoginCallback(string scheme, string returnUrl)
{
var authenticate = await HttpContext.AuthenticateAsync(scheme);
if (authenticate.Succeeded)
User.AddIdentity((ClaimsIdentity)authenticate.Principal.Identity);
return Redirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
}
On the authenticate.Succeeded line, I see that my user was properly authenticated. The User.AddIdentity line properly adds the identity to that user. However, when I look on the subsequent page load, the above identity is gone.
I'm at wits end. Any suggestions would be greatly appreciated. Thanks!
Update 1
Navigating directly to a page that is decorated with [Authorize(AuthenticationSchemes = "AADB2C.OpenIdConnect")] DOES properly result in the page recognizing the user as being authenticated. However, from there, navigating anywhere else then shows them no longer being authenticated.
Update 2
Calling IHttpContextAccessor.HttpContext?.AuthenticateAsync("AADB2C.OpenIdConnect") in places where I couldn't decorate with the Authorize flag (due to requiring access for non-authenticated users as well) properly fetches the authenticated user and their information. So, now the only piece of this puzzle I need to solve is finding a way to get Authorize into areas of the code which I can't access, due to being hidden behind proprietary third-party code.
Update 3
I'm unsure why, but it appears as though if I use AddOpenIdConnect instead of AddMicrosoftIdentityWebApp, it ... works? It defaults to that and my back-end now properly recognizes my authentication.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = null;
options.DefaultSignInScheme = null;
}).AddCookie(options =>
{
options.Events.OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
{
// Syncs user and roles so they are available to the CMS
var synchronizingUserService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(claimsIdentity);
}
};
}).AddOpenIdConnect(options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.Authority = $"MyAuthority";
options.ClientId = "MyClientId";
options.ClientSecret = "MyClientSecret";
options.CallbackPath = "/signin-oidc/aadb2b";
options.Scope.Clear();
options.Scope.Add(OpenIdConnectScope.OpenId);
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
options.Scope.Add(OpenIdConnectScope.Email);
options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles",
NameClaimType = "preferred_username",
ValidateIssuer = false
};
options.Events.OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Response.StatusCode == 401)
{
ctx.HandleResponse();
}
return Task.CompletedTask;
};
options.Events.OnAuthenticationFailed = ctx =>
{
ctx.HandleResponse();
ctx.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(ctx.Exception.Message));
return Task.CompletedTask;
};
});

So, to summarize the steps that I took to resolve this:
Adding [Authorize(AuthenticationSchemes = "MyScheme")] to controllers will properly force authentication when navigating using that controller route.
Calling IHttpContextAccessor.HttpContext?.AuthenticateAsync("MyScheme") returns details of the authenticated principal, allowing code-based control in places where the [Authorize] approach won't work (because it needs to allow both anonymous and authenticated users, and renders differently based on that condition).
For the specific back-end code I couldn't access due to it being hidden behind third-party proprietary code (EPiServer in this case), I was able to resolve the issue by switching to use AddOpenIdConnect instead of AddMicrosoftIdentityWebApp. I'm unsure why this worked, but for the moment I'm not going to question it further.

Related

Unable to redirect uri after cas authentication with WSO2 provider using asp.net core authorization

[enter image description here][1]I am trying to authenticate my .net core application using cas with Wso2 identity provider although the autentication is succesful but on redirect uri i am getting webpage not found error with cas ticket not able to figure out what is the issue any guidance would be helpful
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Login";
options.Events.OnSigningOut = context =>
{
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(
context.HttpContext,
context.Scheme,
context.Options,
context.Properties,
"/"
);
if (builder.Configuration.GetValue("Authentication:CAS:SingleSignOut", false))
{
// Single Sign-Out
var casUrl = new Uri(builder.Configuration["Authentication:CAS:ServerUrlBase"]);
var links = context.HttpContext.RequestServices.GetRequiredService<LinkGenerator>();
var serviceUrl = context.Properties.RedirectUri ?? links.GetUriByPage(context.HttpContext, "/Index");
redirectContext.RedirectUri = UriHelper.BuildAbsolute(
casUrl.Scheme,
new HostString(casUrl.Host, casUrl.Port),
casUrl.LocalPath, "/logout",
QueryString.Create("service", serviceUrl!));
}
context.Options.Events.RedirectToLogout(redirectContext);
return Task.CompletedTask;
};
})
.AddCAS(o =>
{
o.CasServerUrlBase = builder.Configuration["Authentication:CAS:ServerUrlBase"]; // Set in `appsettings.json` file.
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.CasValidationUrl = "/";
});
Login controller
[HttpGet("login")]
public IActionResult Login(string returnUrl)
{
// var props = new AuthenticationProperties { RedirectUri = "/" };
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "CAS");
// return Redirect("http://localhost:5095/");
}
[Web Response](https://i.stack.imgur.com/1LFOP.png)
[1]: https://i.stack.imgur.com/v3fKm.png

Both OIDC and ADFS in same application (OWIN)

I am working on an ASP.NET MVC 5 website using EPiServer CMS.
The requirement is that we have admin (back-end users) log in using ADFS (working) and OIDC for front-end.
Both are set to passive mode and are called through action methods in a controller.
The issue I am facing is that front-end users OIDC is set to (Authentication type ="Federation") when it returns from external call instead of external cookie.
public void Configuration(IAppBuilder app)
{
//IdentityModelEventSource.ShowPII = true;
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11;
ConfigureMemberAuth(app);
ConfigureAdminAuth(app);
}
private void ConfigureMemberAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/logmein/"),
LogoutPath = new PathString("/"),
AuthenticationMode = AuthenticationMode.Active
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
_ = app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ApplicationSettings.OIDCClientId,
ClientSecret = ApplicationSettings.OIDCClientSecret,
Authority = ApplicationSettings.OIDCAuthority,
RedirectUri = $"{ApplicationSettings.Domain}{_oidcRedirectMethod}",
PostLogoutRedirectUri = ApplicationSettings.Domain,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = "xxx",
SaveTokens = true,
AuthenticationMode = AuthenticationMode.Passive,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = notification =>
{
if (string.Equals(notification.ProtocolMessage.Error, "xxx", StringComparison.Ordinal))
{
notification.HandleResponse();
if (string.Equals(notification.ProtocolMessage.ErrorDescription, "xxx", StringComparison.Ordinal))
notification.Response.Redirect(ApplicationSettings.Domain);
else
{
var errorPage = UrlResolver.GetUrl(PageHelper.GetAdminPage().LoginBox.NemIDErrorPage.GetFriendlyUrl());
notification.Response.Redirect(errorPage);
}
}
return Task.CompletedTask;
},
// Retrieve an access token from the remote token endpoint
// using the authorization code received during the current request.
AuthorizationCodeReceived = async notification =>
{
using (var client = new HttpClient())
{
var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
var tokenEndpointResult = await ExchangeCodeForTokens(notification, client, configuration);
// Add the identity token to the returned ClaimsIdentity to make it easier to retrieve.
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.IdToken,
value: tokenEndpointResult.Value<string>(OpenIdConnectParameterNames.IdToken)));
// Retrieve the claims from UserInfo endpoint using the access token as bearer token.
var accesstoken = tokenEndpointResult.Value<string>(OpenIdConnectParameterNames.AccessToken);
var userInfoEndpointResult = await UserInfoEndpointClaims(notification, client, configuration, accesstoken);
//Security note: It is important to verify that the sub claim from ID token matches the sub claim in the UserInfo response
var userinfoSub = userInfoEndpointResult["xx"].Value<string>();
var idtokenSub = notification.AuthenticationTicket.Identity.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
if (userinfoSub == idtokenSub)
{
//add claims from UserInfo endpoint to identity
foreach (var entry in userInfoEndpointResult)
{
if (!notification.AuthenticationTicket.Identity.HasClaim(c => c.Type == entry.Key))
{
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: entry.Key,
value: entry.Value.ToString()));
}
}
// Add access token to claims.
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
OpenIdConnectParameterNames.AccessToken,
accesstoken));
}
}
},
// Attach the id_token stored in the authentication cookie to the logout request.
RedirectToIdentityProvider = notification =>
{
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var token = notification.OwinContext.Authentication.User?.FindFirst(OpenIdConnectParameterNames.IdToken);
if (token != null)
{
notification.ProtocolMessage.IdTokenHint = token.Value;
}
notification.Response.Redirect(ApplicationSettings.Domain);
}
return Task.CompletedTask;
},
}
});
}
private void ConfigureAdminAuth(IAppBuilder app)
{
//Enable cookie authentication, used to store the claims between requests
app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
CookieName = WsFederationAuthenticationDefaults.CookieName,
AuthenticationMode = AuthenticationMode.Active,
CookieHttpOnly = true,
});
//Enable federated authentication
_ = app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
{
//Trusted URL to federation server meta data
MetadataAddress = ApplicationSettings.MetaDataAddress,
//Value of Wtreal must *exactly* match what is configured in the federation server
Wtrealm = ApplicationSettings.RelyPartyUri,
AuthenticationMode = AuthenticationMode.Passive,
Notifications = new WsFederationAuthenticationNotifications()
{
RedirectToIdentityProvider = (ctx) =>
{
// To avoid a redirect loop to the federation server send 403 when user is authenticated but does not have access
if (ctx.OwinContext.Response.StatusCode == 401 && (ctx.OwinContext.Authentication.User.Identity.AuthenticationType == WsFederationAuthenticationDefaults.AuthenticationType && ctx.OwinContext.Authentication.User.Identity.IsAuthenticated))
{
ctx.OwinContext.Response.StatusCode = 403;
ctx.HandleResponse();
}
//XHR requests cannot handle redirects to a login screen, return 401
if (ctx.OwinContext.Response.StatusCode == 401 && IsXhrRequest(ctx.OwinContext.Request))
{
ctx.HandleResponse();
}
return Task.FromResult(0);
},
SecurityTokenValidated = (ctx) =>
{
//Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
if (redirectUri.IsAbsoluteUri)
{
ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
}
//Sync user and the roles to EPiServer in the background
ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);
return Task.FromResult(0);
},
}
});
//Add stage marker to make sure WsFederation runs on Authenticate (before URL Authorization and virtual roles)
app.UseStageMarker(PipelineStage.Authenticate);
// Remap logout to a federated logout
app.Map(LogoutUrl, map =>
{
map.Run(ctx =>
{
ctx.Authentication.SignOut();
ctx.Response.Redirect(ApplicationSettings.Domain);
return Task.FromResult(0);
});
});
}
When I out comment this line: app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
in admin auth then client-login works as intended and authentication type is set to "ExternalCookie" when call return but then admin login stops working ?
Any help would be appreciated

Authorization Code Flow Error in ASP.Net Form using Owin Starup

We are implementing Azure SSO in Traditional ASP.Net Web Application and we want to implement Authorization Code Flow for generating Refresh, Access and Id Tokens.
We have implemented the below code in AuthorizationCodeReceived function of the owin's app.UseOpenIdConnectAuthentication class. From the below mentioned code we are able to successfully fetch the Refreshtoken, AccessToken and IdToken.
But notification.AuthenticationTicket is null and it throws null reference excpetion so we are not able to add the claims for id and access tokens.
Also in the aspx.cs file the HttpContext.Current.User.Identity.IsAuthenticated is returned as false even after generating all the 3 tokens.
Please suggest why notification.AuthenticationTicket is null inside AuthorizationCodeReceived event and what changes we have to do inside AuthorizationCodeReceived event to make HttpContext.Current.User.Identity.IsAuthenticated as "true".
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator dd = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator();
dd.RequireNonce = false;
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
ClientSecret = clientSecret,
PostLogoutRedirectUri = redirectUri,
RedirectUri = redirectUri,
Scope = "openid profile email offline_access",
ResponseType = OpenIdConnectResponseType.Code,
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = (context) =>
{
return System.Threading.Tasks.Task.FromResult(0);
},
AuthorizationCodeReceived = async notification =>
{
using (var client = new HttpClient())
{
var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
[OpenIdConnectParameterNames.ClientId] = notification.Options.ClientId,
[OpenIdConnectParameterNames.ClientSecret] = notification.Options.ClientSecret,
[OpenIdConnectParameterNames.Code] = notification.ProtocolMessage.Code,
[OpenIdConnectParameterNames.GrantType] = "authorization_code",
[OpenIdConnectParameterNames.RedirectUri] = notification.Options.RedirectUri
});
var response = await client.SendAsync(request, notification.Request.CallCancelled);
response.EnsureSuccessStatusCode();
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.AccessToken,
value: payload.Value<string>(OpenIdConnectParameterNames.AccessToken)));
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.IdToken,
value: payload.Value<string>(OpenIdConnectParameterNames.IdToken)));
}
},
// Attach the id_token stored in the authentication cookie to the logout request.
RedirectToIdentityProvider = notification =>
{
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var token = notification.OwinContext.Authentication.User?.FindFirst(OpenIdConnectParameterNames.IdToken);
if (token != null)
{
notification.ProtocolMessage.IdTokenHint = token.Value;
}
}
return Task.CompletedTask;
},
SecurityTokenValidated = (context) =>
{ if (context != null)
{
if (context.ProtocolMessage != null && !string.IsNullOrEmpty(context.ProtocolMessage.IdToken))
{
context.AuthenticationTicket.Identity.AddClaim(new Claim("IdToken", context.ProtocolMessage.IdToken));
}
}
return Task.FromResult(0);
}
}
}
);

Pass user claims to downstream (secondary) APIs?

I have tried searching, but surprisingly cannot find an answer to my question.
I am designing a web app, that will have a front end interface via Angular, with multiple down stream APIs. Like below:
[API - A Client] -> [API - A] -> [API - B]
I am using IdentityServer4 for authentication / authorization. Some users will have a particular claim, lets call it "Foo," and that claim gets correctly passed from the auth server to API A (using Implicit flow) when interacting with API A via the SPA client.
However, I can't get that claim to be passed along from API A to API B, which is using Client Credentials. From what I have read / research, this seems to be correct behavior, since its Client Credential flow.
So my question is, how can I pass a User claim ("Foo"), downstream to a second layer API (API-B)? do i need to use a different flow? Should API-A manually pass it along the request to API-B?
This is my first time using IdentityServer / OpenID connect / OAuth, I am open to changes.
IdentityServer4 Config
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("API-B", "API B")
{
UserClaims = { "Foo" }
},
new ApiResource("API-A", "API A")
{
ApiSecrets = {new Secret("Secret") },
UserClaims = { "Foo", },
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientName = "API-A Client",
ClientId = "API-A_client",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:7900/swagger/oauth2-redirect.html" },
PostLogoutRedirectUris = { "http://localhost:7900/" },
RequireConsent = false,
AllowAccessTokensViaBrowser = true,
AllowedScopes = new List<string>(){
"API-A",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
},
new Client
{
ClientName = "API-A Backend",
ClientId = "API-A_backend",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = {new Secret("Secret".Sha256()) },
AllowedScopes = new List<string>()
{
"API-B",
"custom_resource",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
AlwaysIncludeUserClaimsInIdToken = true,
AlwaysSendClientClaims = true,
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("custom_resource", new [] { "Foo" }),
};
}
}
API A Auth Config
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:6900";
options.ApiName = "API-A";
options.RequireHttpsMetadata = false; // dev only!
});
services.AddTransient<AccessTokenDelegatingHandler>((service) => new AccessTokenDelegatingHandler(tokenEndpoint: $"http://localhost:6900/connect/token", clientId: "API-A", clientSecret: "Secret", scope: "API-B"));
services.AddHttpClient<ApiBHttpClient>(client =>
{
client.BaseAddress = new Uri(Configuration["ApiBUri"]);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<AccessTokenDelegatingHandler>();
API B Auth Config
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:6900";
options.ApiName = "API-B"; // required audience of access tokens
options.RequireHttpsMetadata = false; // dev only!
options.ApiSecret = "Secret";
});
The result above is API-A correctly gets access to "Foo" via IdentityClaims, however API-B does not (although the call is successful).
Any help is appreciated!
Finally found this GitHub page, asking the same question: https://github.com/IdentityServer/IdentityServer4/issues/1679
Which leads here, about extension grants, http://docs.identityserver.io/en/release/topics/extension_grants.html which is my exact scenario.

Web API 2 - block all external calls

Is it possible to block all calls to my web api that are not coming from the web-site itself?
I mean if my MVC app runs at : http://www.domain.com and the web api at http://www.domain.com/api/service, I want the web api to accept calls only from current application only. No external calls allowed.
I will guess maybe a message handler will be the best in this case?
Create a Controller for error page and catch all garbage requests like this:
config.Routes.MapHttpRoute("block", "{*something}", new { controller = "Error", action = "Get" });
You should implement token authorization using a delegating handler.
public class AuthorizationHeaderHandler : DelegatingHandler
{
public AuthorizationHeaderHandler(HttpConfiguration httpConfiguration)
{
//set the inner handler
InnerHandler = new HttpControllerDispatcher(httpConfiguration);
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
IEnumerable<string> apiKeyHeaderValues = null;
if (request.Headers.TryGetValues("X-ApiKey", out apiKeyHeaderValues))
{
var apiKeyHeaderValue = apiKeyHeaderValues.First();
//based on the api-key get username whose session is stil valid.
var username = //code to get user based on apiKeyHeaderValue;
if (!string.IsNullOrEmpty(username))
{
var usernameClaim = new Claim(ClaimTypes.Name, username);
var identity = new ClaimsIdentity(new[] {usernameClaim}, "ApiKey");
var principal = new ClaimsPrincipal(identity);
Thread.CurrentPrincipal = principal;
}
}
else
{
//You don't have an ApiKey from the request... can't proceed
var response = request.CreateResponse(HttpStatusCode.Forbidden,
new {Message = "You are not Authorized to access that resource"}); //new HttpResponseMessage(HttpStatusCode.Forbidden);
var tsc = new TaskCompletionSource<HttpResponseMessage>();
tsc.SetResult(response);
return tsc.Task;
}
return base.SendAsync(request, cancellationToken);
}
}
You can then register the handler in the WebApiConfig
public class WebApiConfig
{
public static void Init(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints:null,
handler: new AuthorizationHeaderHandler(GlobalConfiguration.Configuration)
);
}
}
you can then setup your login controller to authorize a user and assign then a token
public class UserController : ApiController
{
public async Task<HttpResponseMessage> Login([FromBody] UserDTO userDTO)
{
// first perform user authentication.
// clear all existing tokens for this authorized user
//create security token and save token of current user
//You can store this in a database and use a repository to create these.
// Tokens can be guids.
// await token creation
return Request.CreateResponse(HttpStatusCode.OK, new {LogingResult = result, token = token});
}
}
Once that user has the token, it can be used for Api requests by adding to the request header. In Angularjs it can take the following form.
'use strict';
(function () {
angular.module('App', ['ngRoute', 'ngCookies']);
//interceptor for server calls
var httpInterceptor = function ($q, $window, $location) {
return function(promise) {
var success = function(response) {
return response;
};
var error = function(response) {
if (response.status === 403) {
$location.url('/login');
}
return $q.reject(response);
};
return promise.then(success, error);
};
}
httpInterceptor['$inject'] = ['$q', '$window', '$location'];
angular.module('App').factory('httpInterceptor', httpInterceptor);
var api = function ($http, $cookies) {
return {
init: function (token) {
$http.defaults.headers.common['X-ApiKey'] = token || $cookies.token;
}
};
}
api['$inject'] = ['$http', '$cookies'];
angular.module('App').factory('api', api);
})();
Yes this is definitely possible. You have to create a custom handler and filter for the RemoteIpAddress found in the request. Here's an implementation using Owin Self-Host:
public class CustomerHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request?.GetClientIpAddress() != "127.0.0.1")
{
return await Task.FromResult(request.CreateResponse(HttpStatusCode.Unauthorized));
}
return await base.SendAsync(request, cancellationToken);
}
}
public static class HttpReqestMessageExtension
{
public static string GetClientIpAddress(this HttpRequestMessage request)
{
if (!request.Properties.ContainsKey("MS_OwinContext")) return null;
dynamic owinContext = request.Properties["MS_OwinContext"];
return owinContext.Request.RemoteIpAddress;
}
}
If you where using ASP.Net then you would use the appropriate key => MS_HttpContext
Now you simply add this to the startup of your Api:
var config = new HttpConfiguration();
config.MessageHandlers.Add(new CustomerHandler());

Resources