UMA Resource Sharing
This example demonstrates UMA 2.0 (User-Managed Access) — an OAuth-based protocol for resource owner-controlled access. Two sample apps show different architecture patterns.
TIP
For UMA concepts and setup details, see the UMA 2.0 documentation.
Test Users
| Username | Password | Role |
|---|---|---|
alice | alice | Resource Owner — has full access to shared-document |
bob | bob | Requesting Party — no access by default |
Running
dotnet run --project samples/UmaResourceSharing/AppHostThe Aspire dashboard opens automatically. Navigate to either app to interact with the UMA flow.
Blazor + Resource Server
Two-project setup: Blazor Server client communicates with a separate Minimal API resource server. The UmaTokenHandler transparently handles 401+UMA challenge-response between the two.
Resource Server
using System.Security.Claims;
using Duende.AccessTokenManagement;
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Authorization;
using Keycloak.AuthServices.Authorization.AuthorizationServer;
using Keycloak.AuthServices.Common;
using Keycloak.AuthServices.Sdk;
using Keycloak.AuthServices.Sdk.Protection;
using Keycloak.AuthServices.Sdk.Protection.Models;
using Keycloak.AuthServices.Sdk.Protection.Requests;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
builder.AddServiceDefaults();
var resourceServerClientId = "uma-resource-server";
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddKeycloakWebApi(
configuration,
configureJwtBearerOptions: options =>
{
options.Audience = resourceServerClientId;
options.RequireHttpsMetadata = false;
options.MapInboundClaims = false;
}
);
services.AddAuthorization().AddKeycloakAuthorization().AddUmaPermissionTicketChallenge();
services.AddAuthorizationServer(configuration).AddStandardResilienceHandler();
var tokenClientName = ClientCredentialsClientName.Parse("uma_protection");
services.AddDistributedMemoryCache();
services
.AddClientCredentialsTokenManagement()
.AddClient(
tokenClientName,
client =>
{
var options = configuration.GetKeycloakOptions<KeycloakAuthenticationOptions>()!;
client.ClientId = ClientId.Parse(options.Resource);
client.ClientSecret = ClientSecret.Parse(options.Credentials.Secret);
client.TokenEndpoint = new Uri(options.KeycloakTokenEndpoint);
}
);
services
.AddKeycloakProtectionHttpClient(configuration)
.AddClientCredentialsTokenHandler(tokenClientName);
services.AddProblemDetails();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseExceptionHandler();
app.UseStatusCodePages();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/documents/{name}", (string name) => new DocumentResponse(name, $"Content of {name}"))
.RequireProtectedResource("shared-document", "read");
app.MapGet(
"/documents/{name}/details",
(string name) => new DocumentResponse(name, $"Detailed content of {name}")
)
.RequireProtectedResource("shared-document", "write");
app.MapGet(
"/documents",
() => new[] { new DocumentResponse("shared-document", "A document shared via UMA") }
)
.RequireAuthorization();
app.MapPost(
"/permissions/request",
async (
PermissionRequestBody request,
ClaimsPrincipal user,
IKeycloakProtectionClient protectionClient,
IOptions<KeycloakAuthorizationServerOptions> authzOptions
) =>
{
var realm = authzOptions.Value.Realm;
var userId = user.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId))
{
return Results.Unauthorized();
}
var resourceIds = await protectionClient.GetResourcesIdsAsync(
realm,
new GetResourcesRequestParameters { Name = request.Resource, ExactName = true }
);
if (resourceIds.Count == 0)
{
return Results.NotFound(new { Error = $"Resource '{request.Resource}' not found" });
}
var resourceId = resourceIds[0];
// Check if a pending ticket already exists for this user/resource/scope
var existing = await protectionClient.GetPermissionTicketsAsync(
realm,
new GetPermissionTicketsRequestParameters
{
ResourceId = resourceId,
Requester = userId,
}
);
var requestedScopes = request.Scopes ?? ["read"];
var existingScopes = existing.Select(t => t.ScopeName ?? t.Scope).ToHashSet();
var created = new List<string>();
foreach (var scope in requestedScopes)
{
if (existingScopes.Contains(scope))
{
continue;
}
var ticket = new PermissionTicket
{
Resource = resourceId,
Requester = userId,
ScopeName = scope,
Granted = false,
};
// Create via the Permission Ticket API (POST /permission/ticket)
var response = await protectionClient.CreatePermissionTicketWithResponseAsync(
realm,
ticket
);
if (response.IsSuccessStatusCode)
{
created.Add(scope);
}
}
return Results.Ok(
new
{
Message = $"Access requested for [{string.Join(", ", created)}] on '{request.Resource}'",
Scopes = created,
}
);
}
)
.RequireAuthorization();
app.MapGet(
"/permissions/pending",
async (
IKeycloakProtectionClient protectionClient,
IOptions<KeycloakAuthorizationServerOptions> authzOptions
) =>
{
var realm = authzOptions.Value.Realm;
var tickets = await protectionClient.GetPermissionTicketsAsync(
realm,
new GetPermissionTicketsRequestParameters { Granted = false, ReturnNames = true }
);
return Results.Ok(tickets);
}
)
.RequireAuthorization();
app.MapPut(
"/permissions/{id}/approve",
async (
string id,
IKeycloakProtectionClient protectionClient,
IOptions<KeycloakAuthorizationServerOptions> authzOptions
) =>
{
var realm = authzOptions.Value.Realm;
var ticket = new PermissionTicket { Id = id, Granted = true };
await protectionClient.UpdatePermissionTicketAsync(realm, ticket);
return Results.NoContent();
}
)
.RequireAuthorization();
app.MapDelete(
"/permissions/{id}",
async (
string id,
IKeycloakProtectionClient protectionClient,
IOptions<KeycloakAuthorizationServerOptions> authzOptions
) =>
{
var realm = authzOptions.Value.Realm;
await protectionClient.DeletePermissionTicketAsync(realm, id);
return Results.NoContent();
}
)
.RequireAuthorization();
app.MapGet(
"/me",
(ClaimsPrincipal user) =>
new
{
Name = user.Identity?.Name,
Claims = user.Claims.Select(c => new { c.Type, c.Value }),
}
)
.RequireAuthorization();
app.MapDefaultEndpoints();
app.Run();
internal record DocumentResponse(string Name, string Content);
internal record PermissionRequestBody(string Resource, string[]? Scopes);Client App
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Sdk;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using MudBlazor.Services;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
builder.AddServiceDefaults();
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddKeycloakWebApp(
configuration.GetSection(KeycloakAuthenticationOptions.Section),
configureOpenIdConnectOptions: options =>
{
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.RequireHttpsMetadata = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
}
);
services.AddAuthorization();
services.AddCascadingAuthenticationState();
services.AddMudServices();
services.AddHttpContextAccessor();
services.AddKeycloakUmaTicketExchangeHttpClient(configuration);
services
.AddHttpClient(
"ResourceServer",
(sp, client) =>
{
var config = sp.GetRequiredService<IConfiguration>();
var baseUrl =
config["services:resource-server:http:0"]
?? config["ResourceServer:BaseUrl"]
?? "http://localhost:5180";
client.BaseAddress = new Uri(baseUrl);
}
)
.AddUmaTokenHandler();
services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<ClientApp.Components.App>().AddInteractiveServerRenderMode();
app.MapGroup("/authentication").MapLoginAndLogout();
app.MapDefaultEndpoints();
app.Run();Demo Walkthrough
- alice (owner) — Login → Documents → Access (read) → UMA Challenge → RPT → Access Granted
- bob (denied) — Login → Documents → Access (read) → UMA Challenge → Access Denied
- bob requests access → Request Access (read) → "Request submitted" → alice logs in → Approves → bob retries → Access Granted
See sample source code: samples/UmaResourceSharing/ClientApp
Razor Pages (Self-Contained)
Single-project setup: the Razor Pages app is the resource server. Authorization checks and permission ticket management happen inline — no separate API, no UmaTokenHandler.
Program.cs
using Duende.AccessTokenManagement;
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Authorization;
using Keycloak.AuthServices.Common;
using Keycloak.AuthServices.Sdk;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
builder.AddServiceDefaults();
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddKeycloakWebApp(
configuration.GetSection(KeycloakAuthenticationOptions.Section),
configureOpenIdConnectOptions: options =>
{
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.RequireHttpsMetadata = false;
options.MapInboundClaims = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
}
);
var resourceServerClientId = "uma-resource-server";
var resourceServerSecret = "uma-resource-server-secret";
services
.AddAuthorization()
.AddKeycloakAuthorization()
.AddAuthorizationBuilder()
.AddPolicy("UmaRead", policy => policy.RequireProtectedResource("shared-document", "read"))
.AddPolicy("UmaWrite", policy => policy.RequireProtectedResource("shared-document", "write"));
services
.AddAuthorizationServer(options =>
{
configuration
.GetSection(KeycloakAuthenticationOptions.Section)
.BindKeycloakOptions(options);
options.Resource = resourceServerClientId;
options.Credentials = new() { Secret = resourceServerSecret };
options.VerifyTokenAudience = true;
})
.AddStandardResilienceHandler();
var tokenClientName = ClientCredentialsClientName.Parse("uma_protection");
services.AddDistributedMemoryCache();
services
.AddClientCredentialsTokenManagement()
.AddClient(
tokenClientName,
client =>
{
var keycloakOptions =
configuration.GetKeycloakOptions<KeycloakAuthenticationOptions>()!;
client.ClientId = ClientId.Parse(resourceServerClientId);
client.ClientSecret = ClientSecret.Parse(resourceServerSecret);
client.TokenEndpoint = new Uri(keycloakOptions.KeycloakTokenEndpoint);
}
);
services
.AddKeycloakProtectionHttpClient(options =>
{
configuration
.GetSection(KeycloakAuthenticationOptions.Section)
.BindKeycloakOptions(options);
options.Resource = resourceServerClientId;
options.Credentials = new() { Secret = resourceServerSecret };
})
.AddClientCredentialsTokenHandler(tokenClientName);
services.AddHttpContextAccessor();
services.AddRazorPages();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapGroup("/authentication").MapLoginAndLogout();
app.MapDefaultEndpoints();
app.Run();Protected Page — Programmatic Authorization Check
namespace RazorPagesApp.Pages.Documents;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
[Authorize]
public class DetailsModel(IAuthorizationService authorizationService) : PageModel
{
public string DocumentName { get; set; } = default!;
public string Scope { get; set; } = default!;
public string? Content { get; set; }
public bool AccessGranted { get; set; }
public async Task OnGetAsync(string name, string scope = "read")
{
this.DocumentName = name;
this.Scope = scope;
var policyName = scope == "write" ? "UmaWrite" : "UmaRead";
var result = await authorizationService.AuthorizeAsync(this.User, policyName);
if (result.Succeeded)
{
this.AccessGranted = true;
this.Content = scope == "write" ? $"Detailed content of {name}" : $"Content of {name}";
}
}
}Permission Request — Direct Protection API Usage
namespace RazorPagesApp.Pages.Documents;
using System.Security.Claims;
using Keycloak.AuthServices.Authorization.AuthorizationServer;
using Keycloak.AuthServices.Sdk.Protection;
using Keycloak.AuthServices.Sdk.Protection.Models;
using Keycloak.AuthServices.Sdk.Protection.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
[Authorize]
public class IndexModel(
IKeycloakProtectionClient protectionClient,
IOptions<KeycloakAuthorizationServerOptions> authzOptions
) : PageModel
{
public string? Message { get; set; }
public bool IsError { get; set; }
public async Task<IActionResult> OnPostRequestAccessAsync(string scope)
{
var realm = authzOptions.Value.Realm;
var userId = this.User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId))
{
this.Message = "Unable to determine user identity.";
this.IsError = true;
return this.Page();
}
var resourceIds = await protectionClient.GetResourcesIdsAsync(
realm,
new GetResourcesRequestParameters { Name = "shared-document", ExactName = true }
);
if (resourceIds.Count == 0)
{
this.Message = "Resource 'shared-document' not found.";
this.IsError = true;
return this.Page();
}
var resourceId = resourceIds[0];
var existing = await protectionClient.GetPermissionTicketsAsync(
realm,
new GetPermissionTicketsRequestParameters
{
ResourceId = resourceId,
Requester = userId,
}
);
var existingScopes = existing.Select(t => t.ScopeName ?? t.Scope).ToHashSet();
if (existingScopes.Contains(scope))
{
this.Message = $"A request for '{scope}' scope already exists.";
return this.Page();
}
var ticket = new PermissionTicket
{
Resource = resourceId,
Requester = userId,
ScopeName = scope,
Granted = false,
};
var response = await protectionClient.CreatePermissionTicketWithResponseAsync(
realm,
ticket
);
if (response.IsSuccessStatusCode)
{
this.Message =
$"Access request for '{scope}' submitted. The resource owner will review your request.";
}
else
{
this.Message = $"Failed to submit access request ({(int)response.StatusCode}).";
this.IsError = true;
}
return this.Page();
}
}Permission Management — Approve / Deny
namespace RazorPagesApp.Pages.Permissions;
using Keycloak.AuthServices.Authorization.AuthorizationServer;
using Keycloak.AuthServices.Sdk.Protection;
using Keycloak.AuthServices.Sdk.Protection.Models;
using Keycloak.AuthServices.Sdk.Protection.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
[Authorize]
public class IndexModel(
IKeycloakProtectionClient protectionClient,
IOptions<KeycloakAuthorizationServerOptions> authzOptions
) : PageModel
{
public IList<PermissionTicket> Tickets { get; set; } = [];
public string? Message { get; set; }
public bool IsError { get; set; }
public async Task OnGetAsync()
{
await this.LoadTicketsAsync();
}
public async Task<IActionResult> OnPostApproveAsync(string ticketId)
{
var realm = authzOptions.Value.Realm;
var ticket = new PermissionTicket { Id = ticketId, Granted = true };
try
{
await protectionClient.UpdatePermissionTicketAsync(realm, ticket);
this.Message = "Permission approved.";
}
catch
{
this.Message = "Failed to approve permission.";
this.IsError = true;
}
await this.LoadTicketsAsync();
return this.Page();
}
public async Task<IActionResult> OnPostDenyAsync(string ticketId)
{
var realm = authzOptions.Value.Realm;
try
{
await protectionClient.DeletePermissionTicketAsync(realm, ticketId);
this.Message = "Permission request denied.";
}
catch
{
this.Message = "Failed to deny permission.";
this.IsError = true;
}
await this.LoadTicketsAsync();
return this.Page();
}
private async Task LoadTicketsAsync()
{
var realm = authzOptions.Value.Realm;
this.Tickets = await protectionClient.GetPermissionTicketsAsync(
realm,
new GetPermissionTicketsRequestParameters { Granted = false, ReturnNames = true }
);
}
}Demo Walkthrough
- alice (owner) — Login → Documents → Access (read) → Content displayed
- bob (denied) — Login → Documents → Access (read) → "Access Denied" page
- bob requests access → Request Access (read) → "Request submitted" → alice logs in → Permissions → Approve → bob retries → Content displayed
See sample source code: samples/UmaResourceSharing/RazorPagesApp
Keycloak Realm Configuration
{
"realm" : "Test",
"enabled" : true,
"sslRequired" : "none",
"registrationAllowed" : false,
"loginWithEmailAllowed" : true,
"duplicateEmailsAllowed" : false,
"resetPasswordAllowed" : false,
"editUsernameAllowed" : false,
"bruteForceProtected" : false,
"roles" : {
"realm" : [ {
"id" : "f1a1b1c1-0001-0001-0001-000000000001",
"name" : "default-roles-test",
"composite" : true,
"composites" : {
"realm" : [ "offline_access", "uma_authorization" ],
"client" : {
"account" : [ "manage-account", "view-profile" ]
}
}
}, {
"id" : "f1a1b1c1-0001-0001-0001-000000000002",
"name" : "offline_access",
"description" : "${role_offline-access}"
}, {
"id" : "f1a1b1c1-0001-0001-0001-000000000003",
"name" : "uma_authorization",
"description" : "${role_uma_authorization}"
} ]
},
"defaultRole" : {
"id" : "f1a1b1c1-0001-0001-0001-000000000001",
"name" : "default-roles-test",
"composite" : true
},
"clients" : [ {
"id" : "a1000001-0000-0000-0000-000000000001",
"clientId" : "uma-resource-server",
"name" : "UMA Resource Server",
"description" : "Resource server with UMA protection and authorization services",
"enabled" : true,
"clientAuthenticatorType" : "client-secret",
"secret" : "uma-resource-server-secret",
"redirectUris" : [ "*" ],
"webOrigins" : [ "*" ],
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : true,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : true,
"serviceAccountsEnabled" : true,
"authorizationServicesEnabled" : true,
"publicClient" : false,
"frontchannelLogout" : true,
"protocol" : "openid-connect",
"attributes" : {
"oidc.ciba.grant.enabled" : "false",
"backchannel.logout.session.required" : "true",
"oauth2.device.authorization.grant.enabled" : "false",
"display.on.consent.screen" : "false",
"backchannel.logout.revoke.offline.tokens" : "false"
},
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "b1000001-0000-0000-0000-000000000001",
"name" : "audience",
"protocol" : "openid-connect",
"protocolMapper" : "oidc-audience-mapper",
"consentRequired" : false,
"config" : {
"included.client.audience" : "uma-resource-server",
"id.token.claim" : "false",
"lightweight.claim" : "false",
"introspection.token.claim" : "true",
"access.token.claim" : "true"
}
} ],
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"authorizationSettings" : {
"allowRemoteResourceManagement" : true,
"policyEnforcementMode" : "ENFORCING",
"resources" : [ {
"name" : "shared-document",
"type" : "urn:documents",
"ownerManagedAccess" : true,
"displayName" : "Shared Document",
"_id" : "c1000001-0000-0000-0000-000000000001",
"uris" : [ ],
"scopes" : [ {
"name" : "read"
}, {
"name" : "write"
} ]
} ],
"policies" : [ {
"id" : "d1000001-0000-0000-0000-000000000010",
"name" : "owner-policy",
"description" : "Grants access to alice (resource owner)",
"type" : "user",
"logic" : "POSITIVE",
"decisionStrategy" : "UNANIMOUS",
"config" : {
"users" : "[\"e1000001-0000-0000-0000-000000000001\"]"
}
}, {
"id" : "d1000001-0000-0000-0000-000000000011",
"name" : "owner-read-permission",
"type" : "scope",
"logic" : "POSITIVE",
"decisionStrategy" : "UNANIMOUS",
"config" : {
"resources" : "[\"c1000001-0000-0000-0000-000000000001\"]",
"scopes" : "[\"d1000001-0000-0000-0000-000000000001\"]",
"applyPolicies" : "[\"owner-policy\"]"
}
}, {
"id" : "d1000001-0000-0000-0000-000000000012",
"name" : "owner-write-permission",
"type" : "scope",
"logic" : "POSITIVE",
"decisionStrategy" : "UNANIMOUS",
"config" : {
"resources" : "[\"c1000001-0000-0000-0000-000000000001\"]",
"scopes" : "[\"d1000001-0000-0000-0000-000000000002\"]",
"applyPolicies" : "[\"owner-policy\"]"
}
} ],
"scopes" : [ {
"id" : "d1000001-0000-0000-0000-000000000001",
"name" : "read"
}, {
"id" : "d1000001-0000-0000-0000-000000000002",
"name" : "write"
} ],
"decisionStrategy" : "AFFIRMATIVE"
}
}, {
"id" : "a1000001-0000-0000-0000-000000000002",
"clientId" : "uma-client-app",
"name" : "UMA Client App",
"description" : "Blazor Server web app for UMA resource sharing",
"enabled" : true,
"clientAuthenticatorType" : "client-secret",
"secret" : "uma-client-app-secret",
"redirectUris" : [ "*" ],
"webOrigins" : [ "*" ],
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : true,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : true,
"serviceAccountsEnabled" : false,
"authorizationServicesEnabled" : false,
"publicClient" : false,
"frontchannelLogout" : true,
"protocol" : "openid-connect",
"attributes" : {
"oidc.ciba.grant.enabled" : "false",
"backchannel.logout.session.required" : "true",
"oauth2.device.authorization.grant.enabled" : "false",
"display.on.consent.screen" : "false",
"backchannel.logout.revoke.offline.tokens" : "false"
},
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"protocolMappers" : [ {
"id" : "b1000002-0000-0000-0000-000000000001",
"name" : "audience",
"protocol" : "openid-connect",
"protocolMapper" : "oidc-audience-mapper",
"consentRequired" : false,
"config" : {
"included.client.audience" : "uma-resource-server",
"id.token.claim" : "false",
"lightweight.claim" : "false",
"introspection.token.claim" : "true",
"access.token.claim" : "true"
}
}, {
"id" : "b1000002-0000-0000-0000-000000000002",
"name" : "subject",
"protocol" : "openid-connect",
"protocolMapper" : "oidc-sub-mapper",
"consentRequired" : false,
"config" : {
"introspection.token.claim" : "true",
"access.token.claim" : "true"
}
} ],
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email", "basic" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
} ]
}