diff --git a/aspnetcore/blazor/includes/default-scheme.md b/aspnetcore/blazor/includes/default-scheme.md new file mode 100644 index 000000000000..ce05e37f1658 --- /dev/null +++ b/aspnetcore/blazor/includes/default-scheme.md @@ -0,0 +1,2 @@ +> [!NOTE] +> When a single authentication scheme is registered, the authentication scheme is automatically used as the app's default scheme, and it isn't necessary to state the scheme to or via . For more information, see the [ASP.NET Core announcement (aspnet/Announcements #490)](https://github.com/aspnet/Announcements/issues/490). diff --git a/aspnetcore/blazor/security/webassembly/azure-active-directory-groups-and-roles.md b/aspnetcore/blazor/security/webassembly/azure-active-directory-groups-and-roles.md index ce6f8c9113d2..d1f44ab59a8c 100644 --- a/aspnetcore/blazor/security/webassembly/azure-active-directory-groups-and-roles.md +++ b/aspnetcore/blazor/security/webassembly/azure-active-directory-groups-and-roles.md @@ -12,7 +12,7 @@ uid: blazor/security/webassembly/aad-groups-roles This article explains how to configure Blazor WebAssembly to use Azure Active Directory groups and roles. -:::moniker range=">= aspnetcore-6.0" +:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" Azure Active Directory (AAD) provides several authorization approaches that can be combined with ASP.NET Core Identity: @@ -1099,3 +1099,549 @@ Multiple role tests are supported: * :::moniker-end + +:::moniker range=">= aspnetcore-7.0" + +Azure Active Directory (AAD) provides several authorization approaches that can be combined with ASP.NET Core Identity: + +* Groups + * Security + * Microsoft 365 + * Distribution +* Roles + * AAD Administrator Roles + * App Roles + +The guidance in this article applies to the Blazor WebAssembly AAD deployment scenarios described in the following topics: + +* [Standalone with Microsoft Accounts](xref:blazor/security/webassembly/standalone-with-microsoft-accounts) +* [Standalone with AAD](xref:blazor/security/webassembly/standalone-with-azure-active-directory) +* [Hosted with AAD](xref:blazor/security/webassembly/hosted-with-azure-active-directory) + +The article's guidance provides instructions for client and server apps: + +* **CLIENT**: Standalone Blazor WebAssembly apps or the **:::no-loc text="Client":::** app of a hosted Blazor [solution](xref:blazor/tooling#visual-studio-solution-file-sln). +* **SERVER**: Standalone ASP.NET Core server API/web API apps or the **:::no-loc text="Server":::** app of a hosted Blazor solution. + +## Scopes + +To permit [Microsoft Graph API](/graph/use-the-api) calls for user profile, role assignment, and group membership data, the **CLIENT** is configured with (`https://graph.microsoft.com/User.Read`) [Graph API permission (scope)](/graph/permissions-reference) in the Azure portal. + +A **SERVER** app that calls Graph API for role and group membership data is provided `GroupMember.Read.All` (`https://graph.microsoft.com/GroupMember.Read.All`) [Graph API permission (scope)](/graph/permissions-reference) in the Azure portal. + +These scopes are required in addition to the scopes required in AAD deployment scenarios described by the topics listed in the first section of this article. + +> [!NOTE] +> The words "permission" and "scope" are used interchangeably in the Azure portal and in various Microsoft and external documentation sets. This article uses the word "scope" throughout for the permissions assigned to an app in the Azure portal. + +## Group Membership Claims attribute + +In the app's manifest in the Azure portal for **CLIENT** and **SERVER** apps, set the [`groupMembershipClaims` attribute](/azure/active-directory/develop/reference-app-manifest#groupmembershipclaims-attribute) to `All`. A value of `All` results in obtaining all of the security groups, distribution groups, and roles that the signed-in user is a member of. + +1. Open the app's Azure portal registration. +1. Select **Manage** > **Manifest** in the sidebar. +1. Find the `groupMembershipClaims` attribute. +1. Set the value to `All`. +1. Select the **Save** button. + +```json +"groupMembershipClaims": "All", +``` + +## Custom user account + +Assign users to AAD security groups and AAD Administrator Roles in the Azure portal. + +The examples in this article: + +* Assume that a user is assigned to the AAD *Billing Administrator* role in the Azure portal AAD tenant for authorization to access server API data. +* Use [authorization policies](xref:security/authorization/policies) to control access within the **CLIENT** and **SERVER** apps. + +In the **CLIENT** app, extend to include properties for: + +* `Roles`: AAD App Roles array (covered in the [App Roles](#app-roles) section) +* `Wids`: AAD Administrator Roles in [well-known IDs claims (`wids`)](/azure/active-directory/develop/access-tokens#payload-claims) +* `Oid`: Immutable [object identifier claim (`oid`)](/azure/active-directory/develop/id-tokens#payload-claims) (uniquely identifies a user within and across tenants) + +Assign an empty array to each array property so that checking for `null` isn't required when these properties are used in `foreach` loops. + +`CustomUserAccount.cs`: + +```csharp +using System; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +public class CustomUserAccount : RemoteUserAccount +{ + [JsonPropertyName("roles")] + public string[] Roles { get; set; } = Array.Empty(); + + [JsonPropertyName("wids")] + public string[] Wids { get; set; } = Array.Empty(); + + [JsonPropertyName("oid")] + public string Oid { get; set; } +} +``` + +Add a package reference to the **CLIENT** app for [`Microsoft.Graph`](https://www.nuget.org/packages/Microsoft.Graph). + +[!INCLUDE[](~/includes/package-reference.md)] + +Add the Graph SDK utility classes and configuration in the *Graph SDK* section of the article. In the `GraphClientExtensions` class, specify the `User.Read` scope for the access token in the `AuthenticateRequestAsync` method: + +```csharp +var result = await TokenProvider.RequestAccessToken( + new AccessTokenRequestOptions() + { + Scopes = new[] { "https://graph.microsoft.com/User.Read" } + }); +``` + +Add the following custom user account factory to the **CLIENT** app. The custom user factory is used to establish: + +* App Role claims (`appRole`) (covered in the [App Roles](#app-roles) section) +* AAD Administrator Role claims (`directoryRole`) +* An example user profile data claim for the user's mobile phone number (`mobilePhone`) +* AAD Group claims (`directoryGroup`) + +`CustomAccountFactory.cs`: + +```csharp +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Graph; + +public class CustomAccountFactory + : AccountClaimsPrincipalFactory +{ + private readonly ILogger logger; + private readonly IServiceProvider serviceProvider; + + public CustomAccountFactory(IAccessTokenProviderAccessor accessor, + IServiceProvider serviceProvider, + ILogger logger) + : base(accessor) + { + this.serviceProvider = serviceProvider; + this.logger = logger; + } + public override async ValueTask CreateUserAsync( + CustomUserAccount account, + RemoteAuthenticationUserOptions options) + { + var initialUser = await base.CreateUserAsync(account, options); + + if (initialUser.Identity.IsAuthenticated) + { + var userIdentity = (ClaimsIdentity)initialUser.Identity; + + foreach (var role in account.Roles) + { + userIdentity.AddClaim(new Claim("appRole", role)); + } + + foreach (var wid in account.Wids) + { + userIdentity.AddClaim(new Claim("directoryRole", wid)); + } + + try + { + var graphClient = ActivatorUtilities + .CreateInstance(serviceProvider); + + var requestMe = graphClient.Me.Request(); + var user = await requestMe.GetAsync(); + + if (user != null) + { + userIdentity.AddClaim(new Claim("mobilePhone", + user.MobilePhone)); + } + + var requestMemberOf = graphClient.Users[account.Oid].MemberOf; + var memberships = await requestMemberOf.Request().GetAsync(); + + if (memberships != null) + { + foreach (var entry in memberships) + { + if (entry.ODataType == "#microsoft.graph.group") + { + userIdentity.AddClaim( + new Claim("directoryGroup", entry.Id)); + } + } + } + } + catch (ServiceException exception) + { + logger.LogError("Graph API service failure: {Message}", + exception.Message); + } + } + + return initialUser; + } +} +``` + +The preceding code doesn't include transitive memberships. If the app requires direct and transitive group membership claims, replace the `MemberOf` property (`IUserMemberOfCollectionWithReferencesRequestBuilder`) with `TransitiveMemberOf` (`IUserTransitiveMemberOfCollectionWithReferencesRequestBuilder`). + +The preceding code ignores group membership claims (`groups`) that are AAD Administrator Roles (`#microsoft.graph.directoryRole` type) because the GUID values returned by the Microsoft Identity Platform 2.0 are AAD Administrator Role **entity IDs** and not [**Role Template IDs**](/azure/active-directory/roles/permissions-reference#role-template-ids). Entity IDs aren't stable across tenants in Microsoft Identity Platform 2.0 and shouldn't be used to create authorization policies for users in apps. Always use **Role Template IDs** for AAD Administrator Roles **provided by `wids` claims**. + +In `Program.cs` of the **CLIENT** app, configure the MSAL authentication to use the custom user account factory. + +`Program.cs`: + +```csharp +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.Extensions.Configuration; + +... + +builder.Services.AddMsalAuthentication(options => +{ + ... +}) +.AddAccountClaimsPrincipalFactory(); + +... + +builder.Services.AddGraphClient(); +``` + +## Authorization configuration + +In the **CLIENT** app, create a [policy](xref:security/authorization/policies) for each [App Role](#app-roles), AAD Administrator Role, or security group in `Program.cs`. The following example creates a policy for the AAD *Billing Administrator* role: + +```csharp +builder.Services.AddAuthorizationCore(options => +{ + options.AddPolicy("BillingAdministrator", policy => + policy.RequireClaim("directoryRole", + "b0f54661-2d74-4c50-afa3-1ec803f12efe")); +}); +``` + +For the complete list of IDs for AAD Administrator Roles, see [Role template IDs](/azure/active-directory/roles/permissions-reference#role-template-ids) in the Azure documentation. For more information on authorization policies, see . + +In the following examples, the **CLIENT** app uses the preceding policy to authorize the user. + +The [`AuthorizeView` component](xref:blazor/security/index#authorizeview-component) works with the policy: + +```razor + + +

+ The user is in the 'Billing Administrator' AAD Administrator Role + and can see this content. +

+
+ +

+ The user is NOT in the 'Billing Administrator' role and sees this + content. +

+
+
+``` + +Access to an entire component can be based on the policy using an [`[Authorize]` attribute directive](xref:blazor/security/index#authorize-attribute) (): + +```razor +@page "/" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Policy = "BillingAdministrator")] +``` + +If the user isn't logged in, they're redirected to the AAD sign-in page and then back to the component after they sign in. + +A policy check can also be [performed in code with procedural logic](xref:blazor/security/index#procedural-logic). + +`Pages/CheckPolicy.razor`: + +```razor +@page "/checkpolicy" +@using Microsoft.AspNetCore.Authorization +@inject IAuthorizationService AuthorizationService + +

Check Policy

+ +

This component checks a policy in code.

+ + + +

Policy Message: @policyMessage

+ +@code { + private string policyMessage = "Check hasn't been made yet."; + + [CascadingParameter] + private Task authenticationStateTask { get; set; } + + private async Task CheckPolicy() + { + var user = (await authenticationStateTask).User; + + if ((await AuthorizationService.AuthorizeAsync(user, + "BillingAdministrator")).Succeeded) + { + policyMessage = "Yes! The 'BillingAdministrator' policy is met."; + } + else + { + policyMessage = "No! 'BillingAdministrator' policy is NOT met."; + } + } +} +``` + +## Authorize server API/web API access + +A **SERVER** API app can authorize users to access secure API endpoints with [authorization policies](xref:security/authorization/policies) for security groups, AAD Administrator Roles, and App Roles when an access token contains `groups`, `wids`, and `http://schemas.microsoft.com/ws/2008/06/identity/claims/role` claims. The following example creates a policy for the AAD *Billing Administrator* role in `Program.cs` using the `wids` (well-known IDs/Role Template IDs) claims: + +```csharp +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("BillingAdministrator", policy => + policy.RequireClaim("wids", "b0f54661-2d74-4c50-afa3-1ec803f12efe")); +}); +``` + +For the complete list of IDs for AAD Administrator Roles, see [Role template IDs](/azure/active-directory/roles/permissions-reference#role-template-ids) in the Azure documentation. For more information on authorization policies, see . + +Access to a controller in the **SERVER** app can be based on using an [`[Authorize]` attribute](xref:security/authorization/simple) with the name of the policy (API documentation: ). + +The following example limits access to billing data from the `BillingDataController` to Azure Billing Administrators with a policy name of `BillingAdministrator`: + +```csharp +... +using Microsoft.AspNetCore.Authorization; + +[Authorize(Policy = "BillingAdministrator")] +[ApiController] +[Route("[controller]")] +public class BillingDataController : ControllerBase +{ + ... +} +``` + +For more information, see . + +## App Roles + +To configure the app in the Azure portal to provide App Roles membership claims, see [How to: Add app roles in your application and receive them in the token](/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) in the Azure documentation. + +The following example assumes that the **CLIENT** and **SERVER** apps are configured with two roles, and the roles are assigned to a test user: + +* `admin` +* `developer` + +> [!NOTE] +> When developing a hosted Blazor WebAssembly app or a client-server pair of standalone apps (a standalone Blazor WebAssembly app and a standalone ASP.NET Core server API/web API app), the `appRoles` manifest property of both the client and the server Azure portal app registrations must include the same configured roles. After establishing the roles in the client app's manifest, copy them in their entirety to the server app's manifest. If you don't mirror the manifest `appRoles` between the client and server app registrations, role claims aren't established for authenticated users of the server API/web API, even if their access token has the correct roles claims. + +> [!NOTE] +> Although you can't assign roles to groups without an Azure AD Premium account, you can assign roles to users and receive a roles claim for users with a standard Azure account. The guidance in this section doesn't require an AAD Premium account. +> +> Multiple roles are assigned in the Azure portal by **_re-adding a user_** for each additional role assignment. + +The `CustomAccountFactory` shown in the [Custom user account](#custom-user-account) section is set up to act on a `roles` claim with a JSON array value. Add and register the `CustomAccountFactory` in the **CLIENT** app as shown in the [Custom user account](#custom-user-account) section. There's no need to provide code to remove the original `roles` claim because it's automatically removed by the framework. + +In `Program.cs` of a **CLIENT** app, specify the claim named "`appRole`" as the role claim for checks: + +```csharp +builder.Services.AddMsalAuthentication(options => +{ + ... + + options.UserOptions.RoleClaim = "appRole"; +}); +``` + +> [!NOTE] +> If you prefer to use the `directoryRoles` claim (ADD Administrator Roles), assign "`directoryRoles`" to the . + +In `Program.cs` of a **SERVER** app, specify the claim named "`http://schemas.microsoft.com/ws/2008/06/identity/claims/role`" as the role claim for checks: + +```csharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + Configuration.Bind("AzureAd", options); + options.TokenValidationParameters.RoleClaimType = + "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; + }, + options => { Configuration.Bind("AzureAd", options); }); +``` + +[!INCLUDE[](~/blazor/includes/default-scheme.md)] + +> [!NOTE] +> If you prefer to use the `wids` claim (ADD Administrator Roles), assign "`wids`" to the . + +Component authorization approaches are functional at this point. Any of the authorization mechanisms in components of the **CLIENT** app can use the `admin` role to authorize the user: + +* [`AuthorizeView` component](xref:blazor/security/index#authorizeview-component) + + ```razor + + ``` + +* [`[Authorize]` attribute directive](xref:blazor/security/index#authorize-attribute) () + + ```razor + @attribute [Authorize(Roles = "admin")] + ``` + +* [Procedural logic](xref:blazor/security/index#procedural-logic) + + ```csharp + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + + if (user.IsInRole("admin")) { ... } + ``` + +Multiple role tests are supported: + +* Require that the user be in **either** the `admin` **or** `developer` role with the `AuthorizeView` component: + + ```razor + + ... + + ``` + +* Require that the user be in **both** the `admin` **and** `developer` roles with the `AuthorizeView` component: + + ```razor + + + ... + + + ``` + +* Require that the user be in **either** the `admin` **or** `developer` role with the `[Authorize]` attribute: + + ```razor + @attribute [Authorize(Roles = "admin, developer")] + ``` + +* Require that the user be in **both** the `admin` **and** `developer` roles with the `[Authorize]` attribute: + + ```razor + @attribute [Authorize(Roles = "admin")] + @attribute [Authorize(Roles = "developer")] + ``` + +* Require that the user be in **either** the `admin` **or** `developer` role with procedural code: + + ```razor + @code { + private async Task DoSomething() + { + var authState = await AuthenticationStateProvider + .GetAuthenticationStateAsync(); + var user = authState.User; + + if (user.IsInRole("admin") || user.IsInRole("developer")) + { + ... + } + else + { + ... + } + } + } + ``` + +* Require that the user be in **both** the `admin` **and** `developer` roles with procedural code by changing the [conditional OR (`||`)](/dotnet/csharp/language-reference/operators/boolean-logical-operators) to a [conditional AND (`&&`)](/dotnet/csharp/language-reference/operators/boolean-logical-operators) in the preceding example: + + ```csharp + if (user.IsInRole("admin") && user.IsInRole("developer")) + ``` + +Any of the authorization mechanisms in controllers of the **SERVER** app can use the `admin` role to authorize the user: + +* [`[Authorize]` attribute directive](xref:blazor/security/index#authorize-attribute) () + + ```csharp + [Authorize(Roles = "admin")] + ``` + +* [Procedural logic](xref:blazor/security/index#procedural-logic) + + ```csharp + if (User.IsInRole("admin")) { ... } + ``` + +Multiple role tests are supported: + +* Require that the user be in **either** the `admin` **or** `developer` role with the `[Authorize]` attribute: + + ```csharp + [Authorize(Roles = "admin, developer")] + ``` + +* Require that the user be in **both** the `admin` **and** `developer` roles with the `[Authorize]` attribute: + + ```csharp + [Authorize(Roles = "admin")] + [Authorize(Roles = "developer")] + ``` + +* Require that the user be in **either** the `admin` **or** `developer` role with procedural code: + + ```csharp + static readonly string[] scopeRequiredByApi = new string[] { "API.Access" }; + + ... + + [HttpGet] + public IEnumerable Get() + { + HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); + + if (User.IsInRole("admin") || User.IsInRole("developer")) + { + ... + } + else + { + ... + } + + return ... + } + ``` + +* Require that the user be in **both** the `admin` **and** `developer` roles with procedural code by changing the [conditional OR (`||`)](/dotnet/csharp/language-reference/operators/boolean-logical-operators) to a [conditional AND (`&&`)](/dotnet/csharp/language-reference/operators/boolean-logical-operators) in the preceding example: + + ```csharp + if (User.IsInRole("admin") && User.IsInRole("developer")) + ``` + +## Additional resources + +* [Role template IDs (Azure documentation)](/azure/active-directory/roles/permissions-reference#role-template-ids) +* [`groupMembershipClaims` attribute (Azure documentation)](/azure/active-directory/develop/reference-app-manifest#groupmembershipclaims-attribute) +* [How to: Add app roles in your application and receive them in the token (Azure documentation)](/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) +* [Application roles (Azure documentation)](/azure/architecture/multitenant-identity/app-roles) +* +* +* + +:::moniker-end diff --git a/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory-b2c.md b/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory-b2c.md index fd62951ac0e6..059524ccb157 100644 --- a/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory-b2c.md +++ b/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory-b2c.md @@ -143,7 +143,7 @@ The **:::no-loc text="Server":::** app of a hosted Blazor solution created from The `AddAuthentication` method sets up authentication services within the app and configures the JWT Bearer handler as the default authentication method. The method configures services to protect the web API with Microsoft Identity Platform v2.0. This method expects an `AzureAdB2C` section in the app's configuration with the necessary settings to initialize authentication options. ```csharp -services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAdB2C")); ``` @@ -1363,10 +1363,12 @@ The **:::no-loc text="Server":::** app of a hosted Blazor solution created from The `AddAuthentication` method sets up authentication services within the app and configures the JWT Bearer handler as the default authentication method. The method configures services to protect the web API with Microsoft Identity Platform v2.0. This method expects an `AzureAdB2C` section in the app's configuration with the necessary settings to initialize authentication options. ```csharp -services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAdB2C")); ``` +[!INCLUDE[](~/blazor/includes/default-scheme.md)] + and ensure that: * The app attempts to parse and validate tokens on incoming requests. diff --git a/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory.md b/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory.md index 33c1934316bc..c0ebd831420f 100644 --- a/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory.md +++ b/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory.md @@ -136,7 +136,7 @@ The **:::no-loc text="Server":::** app of a hosted Blazor solution created from The `AddAuthentication` method sets up authentication services within the app and configures the JWT Bearer handler as the default authentication method. The method configures services to protect the web API with Microsoft Identity Platform v2.0. This method expects an `AzureAd` section in the app's configuration with the necessary settings to initialize authentication options. ```csharp -services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd")); ``` @@ -1337,10 +1337,12 @@ The **:::no-loc text="Server":::** app of a hosted Blazor solution created from The `AddAuthentication` method sets up authentication services within the app and configures the JWT Bearer handler as the default authentication method. The method configures services to protect the web API with Microsoft Identity Platform v2.0. This method expects an `AzureAd` section in the app's configuration with the necessary settings to initialize authentication options. ```csharp -services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd")); ``` +[!INCLUDE[](~/blazor/includes/default-scheme.md)] + and ensure that: * The app attempts to parse and validate tokens on incoming requests. diff --git a/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md b/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md index ef747ea70ff1..4b3011637438 100644 --- a/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md +++ b/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md @@ -1893,6 +1893,8 @@ The `Startup` class has the following additions. .AddIdentityServerJwt(); ``` + [!INCLUDE[](~/blazor/includes/default-scheme.md)] + * In `Program.cs`: * The IdentityServer middleware exposes the OpenID Connect (OIDC) endpoints: