How to user Azure ad SSO B2B with "Microsoft.Graph" instead "Microsoft.Azure.ActiveDirectory.GraphClient" - azure-active-directory

I'm creating an application for my web app but I want to only use Microsoft.Graph instead ActiveDirectory.GraphClient that is possible,if yes then how?
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
NameClaimType = ClaimTypes.Name,
RoleClaimType = ClaimTypes.Role,
};
options.Scope.Add("openid profile User.ReadWrite User.ReadBasic.All Sites.ReadWrite.All Contacts.ReadWrite People.Read Notes.ReadWrite.All Tasks.ReadWrite Mail.ReadWrite Files.ReadWrite.All Calendars.ReadWrite");
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.Redirect("/Error");
context.HandleResponse(); // Suppress the exception
return Task.CompletedTask;
},
};
});

The simplest answer for you is to follow the "Get Started of ASPNET", and then change the logic to suit your requirement.
Do it your by yourself:
Use the Nuget to install the "Microsoft.Graph" and then modify the GraphScopes in configuration file of project(appsettings.json for NETCore).
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"CallbackPath": "/signin-oidc",
"BaseUrl": "https://localhost:44334",
"ClientId": "your client id",
"ClientSecret": "your secret", // This sample uses a password (secret) to authenticate. Production apps should use a certificate.
"Scopes": "openid email profile offline_access",
"GraphResourceId": "https://graph.microsoft.com/",
"GraphScopes": "User.Read User.ReadBasic.All Mail.Send
}
Modify the configure service code as below:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();

Related

IdentityServer4: Google sign-in only show OpenID scope and didn't show the email and profile scope information

When i'm trying to sign-in with google sign-in from blazor webassembly, the requested claims and the claims returned from identity server is not matching (see output)
BackEnd/Config.cs
namespace BackEnd
{
public static class ServerConfiguration
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("roles", "User roles", new List<string> { "role" })
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("protectedScope", "Protected Scope")
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client()
{
ClientId = <confidential>,
ClientName = "client 1",
RequireClientSecret = false,
RequirePkce = true,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
AllowOfflineAccess = true,
AllowedScopes = new List<string>{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"protectedScope"
}
},
new Client()
{
ClientId = "blazor",
ClientName = "oidcUser",
RequireClientSecret = false,
RequirePkce = true,
RequireConsent = true,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedScopes = new List<string>{
"openid",
"email",
"profile",
"protectedScope"
}
}
};
public static List<TestUser> TestUsers {
get
{
TestUser user1 = new TestUser()
{
SubjectId = "2f47f8f0-bea1-4f0e-ade1-88533a0eaf57",
Username = "John",
Claims = new List<Claim>()
{
new Claim("role", "SignedInUser"),
new Claim("email", "johnsmith#gmail.com"),
new Claim("picture", "https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.business2community.com%2Fsocial-media%2Fimportance-profile-picture-career-01899604&psig=AOvVaw2LC5T-WZMYnHD9I7PeK7lT&ust=1615219065948000&source=images&cd=vfe&ved=2ahUKEwip1caGxp7vAhV1NbcAHd_2BFwQjRx6BAgAEAc")
}
};
List<TestUser> testUsers = new List<TestUser>();
testUsers.Add(user1);
return testUsers;
}
}
}
}
BackEnd/Startup.cs
namespace BackEnd
{
public class Startup
{
public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
private string _clientId = null;
private string _clientSecret = null;
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
var cert = new X509Certificate2(Path.Combine(".", "IdsvCertificate.pfx"), "YouShallNotPass123");
_clientId = Configuration["OAuth:ClientId"];
_clientSecret = Configuration["OAuth:ClientSecret"];
services.AddControllersWithViews();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IProfileService, ProfileService>();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
options.UserInteraction = new UserInteractionOptions() { LoginUrl = "/Account/Login", LogoutUrl = "/Account/Logout" };
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddProfileService<ProfileService>()
.AddAspNetIdentity<ApplicationUser>();
builder.AddSigningCredential(cert);
// builder.AddDeveloperSigningCredential();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = _clientId;
options.ClientSecret = _clientSecret;
options.ClaimActions.MapJsonKey("picture", "picture", "url");
options.SaveTokens = true;
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.Authority = "https://accounts.google.com";
options.RequireHttpsMetadata = true;
options.ResponseType = "code";
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("openid");
options.ClientId = _clientId;
options.ClientSecret = _clientSecret;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("picture", "picture", "url");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
ValidateIssuer = true
};
});
services.AddAuthorization();
services.AddGrpc();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<UserService>();
endpoints.MapDefaultControllerRoute().RequireAuthorization();
});
}
}
}
BackEnd/ProfileService.cs
public class ProfileService : IProfileService
{
public ProfileService()
{
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
context.IssuedClaims.AddRange(roleClaims);
await Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
await Task.CompletedTask;
}
}
FrontEnd/Program.cs
namespace FrontEnd
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient()
{ BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(services =>
{
var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
var channel = GrpcChannel.ForAddress("https://localhost:5000", new GrpcChannelOptions
{
HttpHandler = httpHandler
});
return new Greeter.GreeterClient(channel);
});
builder.Services.AddScoped(services =>
{
var baseAddressMessageHandler = services.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
var channel = GrpcChannel.ForAddress("https://localhost:5000", new GrpcChannelOptions
{
HttpHandler = httpHandler
});
return new User.UserClient(channel);
});
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Authentication:Google", options.ProviderOptions);
options.UserOptions.RoleClaim = "SignedInUser";
}).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
await builder.Build().RunAsync();
}
}
FrontEnd/wwwroot/appsettings.json
{
"Authentication":{
"Google": {
"Authority": "https://localhost:5000",
"ClientId": <confidential>,
"ClientSecret": "2fxc9srOe8QsRBnhzLIa1pF0",
"DefaultScopes": [
"email",
"profile",
"openid"
],
"PostLogoutRedirectUri": "https://localhost:5001/",
"RedirectUri": "https://localhost:5001/authentication/login-callback",
"ResponseType": "code"
},
}
This is the output from IdentityServer when i'm trying to sign-in
[18:34:28 Debug] IdentityServer4.Validation.TokenValidator
Calling into custom token validator: IdentityServer4.Validation.DefaultCustomTokenValidator
[18:34:28 Debug] IdentityServer4.Validation.TokenValidator
Token validation success
{"ClientId": null, "ClientName": null, "ValidateLifetime": true, "AccessTokenType": "Jwt", "ExpectedScope": "openid", "TokenHandle": null, "JwtId": "8E6167D64F8FEA2FF6D12D17A1CEEBFE", "Claims": {"nbf": 1615548868, "exp": 1615552468, "iss": "https://localhost:5000", "aud": "https://localhost:5000/resources", "client_id": "499675830263-ldcg4fm7kcbjlt48tpaffqdbfnskmi8v.apps.googleusercontent.com", "sub": "81c306df-c1f0-4714-964f-2459b670429e", "auth_time": 1615548849, "idp": "oidc", "jti": "8E6167D64F8FEA2FF6D12D17A1CEEBFE", "sid": "0BF0BA4CA6BD3DE8D158A426A70A91E0", "iat": 1615548868, "scope": ["openid", "profile", "email", "role"], "amr": "external"}, "$type": "TokenValidationLog"}
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Creating userinfo response
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Scopes in access token: openid profile email role
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Requested claim types: sub name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at email email_verified role
[18:34:28 Information] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Profile service returned the following claim types: sub name preferred_username
[18:34:28 Debug] IdentityServer4.Endpoints.UserInfoEndpoint
End userinfo request
[18:34:29 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /connect/checksession matched to endpoint type Checksession
[18:34:29 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Checksession, successfully created handler: IdentityServer4.Endpoints.CheckSessionEndpoint
[18:34:29 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.CheckSessionEndpoint for /connect/checksession
[18:34:29 Debug] IdentityServer4.Endpoints.CheckSessionEndpoint
Rendering check session result
From the output, the requested claims and the claims that's returned by the profile service is not matching
From the image, there should be a name after the "hello"
The image is similar except now it shows the name. IdentityServer4 is working fine, it shows everything as i expected
By default, only these claims are mapped from the ID-Token:
options.ClaimActions.MapUniqueJsonKey("sub", "sub");
options.ClaimActions.MapUniqueJsonKey("name", "name");
options.ClaimActions.MapUniqueJsonKey("given_name", "given_name");
options.ClaimActions.MapUniqueJsonKey("family_name", "family_name");
options.ClaimActions.MapUniqueJsonKey("profile", "profile");
options.ClaimActions.MapUniqueJsonKey("email", "email");
To get additional claims mapped, we have to write like:
options.ClaimActions.MapUniqueJsonKey("website", "website");
options.ClaimActions.MapUniqueJsonKey("gender", "gender");
options.ClaimActions.MapUniqueJsonKey("birthdate", "birthdate");
In AddOpenIDConnect. This is called Claims transformation if you want to google for it.

Logout from client doesn't work. Identity Server4

I am currently implementing OAuth Server with IdentityServer4 using .NET Core 3.1 and React for client SPA.
When I click logout I get the following:
React JS:
const handleLogout = async () => {
const token = sessionStorage.getItem("id_token");
userManager.signoutRedirect({
id_token_hint: token
});
};
IdentityServer4 Configuration:
new Client
{
ClientId = _mobileAuthorizationCodeClientId,
ClientName = _mobileAuthorizationCodeClientName,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RequireConsent = false,
AllowAccessTokensViaBrowser = true,
AllowOfflineAccess = true,
AllowedScopes =
{
_avlApi, _clearingApi, _reportingApi, _assetManagementApi, _ticketingApi,
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
},
RedirectUris = { "https://localhost:3000/signin-callback" },
PostLogoutRedirectUris = { "https://localhost:3000/signout-callback" },
AllowedCorsOrigins = { "https://localhost:3000" },
},
Startup.cs relevant parts:
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.Password.RequiredLength = 4;
config.Password.RequireDigit = false;
config.Password.RequireNonAlphanumeric = false;
config.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer(options =>
{
options.IssuerUri = publicOrigin;
options.PublicOrigin = publicOrigin;
options.UserInteraction = new UserInteractionOptions()
{
LogoutUrl = "/account/logout",
LoginUrl = "/account/login",
LoginReturnUrlParameter = "returnUrl",
CustomRedirectReturnUrlParameter = "returnUrl",
};
})
.AddAspNetIdentity<ApplicationUser>()
.AddInMemoryIdentityResources(Config.GetResources())
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryClients(Config.GetClients())
.AddDeveloperSigningCredential()
.AddProfileService<IdentityProfileService>();
services.AddAuthentication();
I don't see any error logs from IDP.
I've tried to get some workaround around similar issue. https://github.com/IdentityServer/IdentityServer4/issues/3854
The weird thing. If connect/endsession is not canceled - the logout works as expected.
We using https://github.com/maxmantz/redux-oidc for client react js.
Versions:
<PackageReference Include="IdentityServer4" Version="3.1.3" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.3" />
Question is: why connect/endsession is cancelled?
Any information will be highly appreciated!
Are you missing an await on the below line?
await userManager.signoutRedirect({
id_token_hint: token
});
usually, the requests will be canceled when the user gets redirected to a new page, I see the subsequent call after the canceled call is authorize which will redirect the user...
hopefully adding await may solve the problem.

Missing "aud" claim in access token

For unknown reason to me the "aud" claim is not present in access token (it is present in id token though).
Once access token is being sent to the API i get the following error:
Bearer was not authenticated. Failure message: IDX10214: Audience
validation failed. Audiences: 'empty'. Did not match:
validationParameters.ValidAudience: 'productconfigurationapi' or
validationParameters.ValidAudiences: 'null'.
I know i can turn off audience validation and everything works then but i don't get why "aud" is not part of the access token.
Here's my IS4 configuration:
the client:
new Client
{
ClientId = "Spa",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenType = AccessTokenType.Jwt,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"productconfigurationapi"
},
RequireConsent = false
}
the api resource:
new ApiResource("productconfigurationapi")
{
UserClaims =
{
JwtClaimTypes.Audience
}
}
the API Scope:
return new List<ApiScope>
{
new ApiScope("productconfigurationapi")
};
and here's how IS4 is configured within its host application:
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddConfigurationStore(options =>
{
})
.AddOperationalStore(options =>
{
})
.AddAspNetIdentity<IdentityUser>()
.AddJwtBearerClientAuthentication();
You should tie the ApiScope to the ApiResource by setting the Scopes property:
var api = new ApiResource("productconfigurationapi")
{
UserClaims =
{
//...optional user claims...
},
Scopes = new List<string>
{
"productconfigurationapi"
},
};
To complement this answer, I write a blog post that goes into more detail about this topic:
IdentityServer – IdentityResource vs. ApiResource vs. ApiScope

Authentication cookie disappears in react SPA after some time

We have an SPA, written in React together with ASP.net core for hosting.
To authenticate the app, we are using IdentityServer4 and use a cookie. The client is configured according to the sample, described here: https://github.com/IdentityServer/IdentityServer4/tree/main/samples/Quickstarts/4_JavaScriptClient/src
For authenticating a user, everythings works fine. It will be redirected to the login page. After signing in, redirection to the SPA is done. The authentiation cookie is set as expected with:
HttpOnly = true
Secure = true
SameSite = None
Expires / Max-age = one week from login time
The cookie is used also in other MVC (.net core and MVC 5) applications for authentication reasons.
In the SPA, we are also using SignalR, which needs the cookie for authentication.
Our issue:
After about 30 minutes of idle time in the browser and either doing a refresh or navigating, the authentication cookie (and only that, other remains) is disappearing from the browser automatically. Then the user has to sign in again. Why does this happen together with the SPA?
Code
Complete code can be found in github
Client
Snippets of UserService.ts
const openIdConnectConfig: UserManagerSettings = {
authority: baseUrls.person,
client_id: "js",
redirect_uri: joinUrl(baseUrls.spa, "signincallback"),
response_type: "code",
scope: "openid offline_access profile Person.Api Translation.Api",
post_logout_redirect_uri: baseUrls.spa,
automaticSilentRenew: true
};
export const getUserService = asFactory(() => {
const userManager = new UserManager(openIdConnectConfig);
return createInstance(createStateHandler(defaultUserState), userManager, createSignInProcess(userManager));
}, sameInstancePerSameArguments());
Server
Snipped of Startup.cs
public void ConfigureServices(IServiceCollection services)
{
Log.Information($"Start configuring services. Environment: {_environment.EnvironmentName}");
services.AddControllersWithViews();
services.AddIdentity<LoginInputModel, RoleDto>()
.AddDefaultTokenProviders();
var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddSigningCredential(certificate)
.AddProfileService<ProfileService>()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));
if (_environment.IsDevelopment())
{
identityServerBuilder.AddDeveloperSigningCredential();
}
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(_sharedAuthTicketKeys))
.SetApplicationName("SharedCookieApp");
services.AddAsposeMailLicense(Configuration);
var optionalStartupSettings = SetupStartupSettings();
if (optionalStartupSettings.IsSome)
{
var settings = optionalStartupSettings.Value;
services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = new PathString("/Account/AccessDenied");
options.Cookie.Name = ".AspNetCore.Auth.Cookie";
options.Cookie.Path = "/";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.LoginPath = new PathString("/account/login");
options.Cookie.SameSite = SameSiteMode.None;
});
var authBuilder = services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "Identity.Application"; });
authBuilder = ConfigureSaml2(authBuilder, settings);
authBuilder = ConfigureGoogle(authBuilder);
authBuilder.AddCookie();
}
else
{
throw new InvalidOperationException($"Startup settings are not configured in appsettings.json.");
}
SetupEntityFramework(services);
}
Snippet of identity server client config from appsettings.json
{
"Enabled": true,
"ClientId": "js",
"ClientName": "JavaScript Client",
"AllowedGrantTypes": [ "authorization_code" ],
"RequirePkce": true,
"RequireClientSecret": false,
"RedirectUris": [ "https://dev.myCompany.ch/i/signincallback", "https://dev.myCompany.com/i/signincallback", "https://dev.myCompany.de/i/signincallback" ],
"PostLogoutRedirectUris": [ "https://dev.myCompany.ch/i/", "https://dev.myCompany.com/i/", "https://dev.myCompany.de/i/" ],
"AllowedCorsOrigins": [],
"AllowedScopes": [ "openid", "offline_access", "profile", "Translation.Api", "Person.Api" ],
"RequireConsent": false,
"AllowOfflineAccess": true
}
Update
In the meantime I discovered that the cookie, while requesting https://ourdomain/.well-known/openid-configuration after 30 minutes idle time, has lost the values of Domain, Path, Expires/Max-Age, HttpOnly, Secure and SameSite.None. Those values have definitely been set after signing in.
The response cookie has the value of Expires/Max-Age set to a time in the past and therefore the cookie will be dropped by the browser.
Has anyone an idea, why those values got lost after some time?
Finally, I figured out how to tackle this.
It had to do with the configuration of IdentityServer. The missing part was the method AddAspNetIdentity<LoginInputModel>().
Before:
var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddSigningCredential(certificate)
.AddProfileService<ProfileService>()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));
Now:
var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddSigningCredential(certificate)
.AddAspNetIdentity<LoginInputModel>()
.AddProfileService<ProfileService>()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));
With that additional configuration line, Identity Server is handling the cookie correctly.
You can add below code in Configure method in Startup.cs :
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
Secure = CookieSecurePolicy.Always,
MinimumSameSitePolicy=SameSiteMode.None
});
add this before using identity server service:
app.UseIdentityServer();
You need to check "HttpOnly" option for your situation because it can give trouble with oidc client on react.

How to access [Authorize] controller actions using HttpClient with Bearer token? Getting 401 "The audience is invalid"

There are four clients within the application:
angular.application - resource owner
identity_ms.client - webapi app (.net core 2.1)
IdentityServer4 with AspNetIdentity
AccountController with shared actions to register users, reset password etc.
UserController with secured actions.
The Data action of the UserController has an [Authorize(Policy = "user.data")] attribute
ms_1.client - webapi app (.net core 2.1)
request.client - added specially to send requests from ms_1.client to identity_ms.client's UserController to get some user data.
I'm requesting clients using Postman:
http://localhost:identity_ms_port/connect/token to get access_token
http://localhost:ms_1_port/api/secured/action to get some secured data from ms_1
http://localhost:identity_ms_port/api/user/data to get some secured user data from identity_ms
Everything is working fine.
Also, ms_1 service has a secured action requesting http://localhost:identity_ms_port/api/user/data using System.Net.Http.HttpClient.
// identity_ms configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddApplicationPart(/*Assembly*/)
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
var clients = new List<Client>
{
new Client
{
ClientId = "angular.application",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope", "identity_ms.scope" },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword
},
new Client
{
ClientId = "ms_1.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials
},
new Client
{
ClientId = "identity_ms.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes =
{
"user.data.scope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
AllowedGrantTypes = GrantTypes.Implicit
},
new Client
{
ClientId = "request.client",
AllowedScopes = { "user.data.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
}
}
};
var apiResources = new List<ApiResource>
{
new ApiResource("ms_1.scope", "MS1 microservice scope"),
new ApiResource("identity_ms.scope", "Identity microservice scope"),
new ApiResource("user.data.scope", "Requests between microservices scope")
};
var identityResources = new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
services
.AddAuthorization(options => options.AddPolicy("user.data", policy => policy.RequireScope("user.data.scope")))
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(identityResources)
.AddInMemoryApiResources(apiResources)
.AddInMemoryClients(clients);
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "identity_ms.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
services.AddSwaggerGen(/*swagger options*/);
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseIdentityServer();
app.UseAuthentication();
app.UseCors("Policy");
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = "ms_1.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseAuthentication();
app.UseCors("Policy");
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client action using HttpClient
[HttpPost]
public async Task<IActionResult> Post(ViewModel model)
{
//...
using (var client = new TokenClient("http://localhost:identity_ms_port/connect/token", "ms_1.client", "secret"))
{
var response = await client.RequestClientCredentialsAsync("user.data.scope");
if (response.IsError)
{
throw new Exception($"{response.Error}{(string.IsNullOrEmpty(response.ErrorDescription) ? string.Empty : $": {response.ErrorDescription}")}", response.Exception);
}
if (string.IsNullOrWhiteSpace(response.AccessToken))
{
throw new Exception("Access token is empty");
}
var udClient = new HttpClient();
udClient.SetBearerToken(response.AccessToken);
udClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var result = await udClient.GetAsync("http://localhost:identity_ms_port/api/user/data");
}
//...
}
I've tried the following:
To retrieve access_token from the request to ms_1 Authorization header and use it to access user/data.
To get new access_token to access user/data with it.
See public async Task<IActionResult> Post(ViewModel model) code within the code block.
In both cases, I've got the correct token which I can use to request both secured/action and user/data actions from Postman, but HttpClient is getting Unauthorized response (401).
Response headers screenshot
What am I doing wrong?
In your client code with HttpClient you are not requesting any scopes for the API therefore the token that is issued by Identity Server 4 will not contain the API as one of the audiences and then subsequently you will get 401 from API.
Change your token request to ask for the API scope as well.
var response = await client.RequestClientCredentialsAsync("user.data.scope ms_1.scope");

Resources