Open Closed

We would like to use a central ABP.io Auth Server and configure another project's API Host to obtain tokens from this central Auth Server. #9993


User avatar
0
cetin.sahin created

We would like to use a central ABP.io Auth Server and configure another project's API Host to obtain tokens from this central Auth Server.

For this purpose, we created two separate solutions: SSO and ReactAppService. We added the application to the Auth Server as React_Client. We configured the ReactAppService project's API Host to use the Auth Server in the SSO solution.

However, when we try to get a token via Postman and call the /api/identity/users POST method, we receive an authorization error.

React client Host api appseting

Token postman

create abp user sample


17 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent

    To use a central ABP Auth Server and have another project's API Host accept tokens from this Auth Server, you must ensure the following key points are correctly configured:

    1. API Resource Registration in Auth Server:

      • The API Host (ReactAppService) must be registered as an API resource in your central Auth Server (SSO).
      • In the Auth Server, add a new API resource (e.g., ReactAppService) and include the required scopes (such as ReactAppService, email, profile, etc.).
      • The client (React_Client) must be allowed to request the API resource's scope.
    2. Scope Usage in Token Request:

      • When requesting a token, the scope parameter must include the API resource scope (e.g., scope=ReactAppService offline_access), not just the client or SSO scope.
      • If you only request MerkeziSSO offline_access, the access token may not be valid for the API Host.
    3. API Host Configuration:

      • In the API Host's appsettings.json, set the AuthServer:Authority to the central Auth Server's URL.
      • Ensure the AuthServer:RequireHttpsMetadata is set according to your environment.
      • The API Host must be configured to validate tokens issued by the central Auth Server and recognize the correct audience (API resource).
    4. Audience Validation:

      • The API Host must validate the aud (audience) claim in the JWT. This must match the API resource name you registered in the Auth Server.
      • If the audience does not match, you will get a 401 Unauthorized error.
    5. Token Usage:

      • When calling the API Host, use the access token obtained from the Auth Server in the Authorization: Bearer header.

    Common Issues and Solutions:

    • 401 Unauthorized: Usually caused by missing or incorrect audience in the token, or the API Host not configured to accept the token from the central Auth Server.
    • Missing API Resource: Ensure the API Host is registered as an API resource in the Auth Server and the client is allowed to request its scope.

    Example Steps:

    1. Register API Resource in Auth Server:
    "ApiResources": {
         "ReactAppService": {
           "Scopes": [ "ReactAppService" ]
         }
       }
    
    1. Register Scope for Client:
    "Clients": {
         "React_Client": {
           "Scopes": [ "ReactAppService", "email", "profile", "offline_access" ]
         }
       }
    
    1. Configure API Host:
    "AuthServer": {
         "Authority": "https://localhost:44387",
         "RequireHttpsMetadata": true,
         "ApiName": "ReactAppService"
       }
    
    1. Request Token with Correct Scope:
    POST /connect/token
       client_id=React_Client
       client_secret=1q2w3E*
       grant_type=client_credentials
       scope=ReactAppService offline_access
    
    1. Call API with Token:
    Authorization: Bearer {access_token}
    

    Summary Checklist:

    • [ ] API Host is registered as an API resource in Auth Server.
    • [ ] Client is allowed to request the API Host's scope.
    • [ ] Token request includes the correct API resource scope.
    • [ ] API Host is configured to validate tokens from the central Auth Server and checks the correct audience.
    • [ ] Access token is sent in the Authorization header when calling the API.

    If you follow these steps and ensure the scopes and audiences are correct, your API Host should accept tokens from the central Auth Server.

    No verified source available for this specific query.


    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

  • User Avatar
    0
    cetin.sahin created

    this error stil get

  • User Avatar
    0
    cetin.sahin created

    The ticket I opened doesn't appear to be assigned to anyone.

    The issue is urgent.

    Can you help?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Could you share the full logs? and your ReactAppServiceReactAppService Module class. thanks.

  • User Avatar
    0
    cetin.sahin created

    < Deleted Link >

  • User Avatar
    0
    cetin.sahin created

    we upload all project

  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Hi!

    I fixed your project and there is the take-aways:

    • Created 2 different client in the "MerkeziSSO" application's OpenIddictDataSeedContributor.cs file and added required parameters in appsettings.json file of MerkeziSSO.DbMigrator project.

    • Reorganized appsettings.json files of both projects TestClient.Blazor and TestClient.HttpApi.Host to use MerkeziSSO.AuthServer project.

      • Used MerkeziSSO scope instead TestClient scope in TestClient.Blazor and TestClient.HttpApi.Host projects.
      • There is a ClientSecret mismatch in Blazor project, I fixed it:
      • There is a ClientSecret mismatch in Blazor project, I fixed it:
    • I updated RedirectAllowedUrls section of MerkeziSSO.AuthServer project in their appsettings.json file to allow redirection to TestClient projects after authentication.

    • String Encryption PassPhrase was different for both projects since they're created separately. I set the exact same PassPhrase from MerkeziSSO.AuthServer for TestClient.Blazor and TestClient.HttpApi.Host projects to read token without any problem

    ⚠️ Make sure your Redis cache is completely cleaned and make sure your database is seeded from scratch (at least OpenId Applications table)

    I've sent fixed version of your project to your email address, you can check it from there.

    In your scenario, you cannot retrieve authenticated user information from the database since your TestClient.HttpApi.Host uses different database and the same user id doesn't exist that database, so CurrentUser will be always null unless you use the same database or duplicate users in to that new application's database. Or use HttpApi.Client packages of Account module insted direct using Application layer

  • User Avatar
    0
    cetin.sahin created

    I replied your email. we still get "403 forbidden " for client app api

  • User Avatar
    0
    cetin.sahin created

    Our problem is still ongoing. Is there anything else we need to configure to use centralized SSO?

  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Problem Summary

    You want to use a central ABP Auth Server (SSO solution) and configure another project's API Host (ReactAppService) to obtain and validate tokens from this central Auth Server. The user is experiencing a 403 Forbidden error when calling API endpoints even though authentication (401) is passing.

    Root Cause Analysis

    Based on the ABP source code and the ticket conversation, there are several configuration requirements that must be met:

    1. Authentication vs Authorization

    • 401 Unauthorized: Authentication failed (no valid token or token not accepted)
    • 403 Forbidden: Authentication succeeded but authorization failed (user doesn't have required permissions)

    The fact that you're getting 403 means the token is being accepted, but the user lacks the necessary permissions in the API application's database.

    2. Permission Storage Issue

    When you use a separate database for your API application, ABP's permission system cannot find the user's permissions because:

    • User exists in central SSO database with permissions
    • API application uses a different database without that user or their permissions
    • CurrentUser is populated from JWT claims (user ID, username, roles)
    • Permission checks fail because IPermissionChecker queries the local database

    Complete Solution

    Step 1: Configure OpenIddict Applications in Central Auth Server

    In your MerkeziSSO (Central SSO) solution's OpenIddictDataSeedContributor.cs:

    public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDependency
    {
        // ... other code ...
    
        public async Task SeedAsync(DataSeedContext context)
        {
            await CreateApiScopesAsync();
            await CreateClientsAsync();
        }
    
        private async Task CreateApiScopesAsync()
        {
            // Create a scope for your API application
            await CreateScopesAsync(new OpenIddictScopeDescriptor
            {
                Name = "TestClient",  // This is your API resource scope
                DisplayName = "Test Client API",
                Description = "Test Client API Scope",
                Resources =
                {
                    "TestClient"  // API resource name
                }
            });
    
            // Standard OpenID scopes (usually already exist)
            await CreateScopesAsync(new OpenIddictScopeDescriptor
            {
                Name = OpenIddictConstants.Scopes.Email,
                DisplayName = "Email",
                Resources = { }
            });
    
            await CreateScopesAsync(new OpenIddictScopeDescriptor
            {
                Name = OpenIddictConstants.Scopes.Profile,
                DisplayName = "Profile", 
                Resources = { }
            });
    
            await CreateScopesAsync(new OpenIddictScopeDescriptor
            {
                Name = OpenIddictConstants.Scopes.Roles,
                DisplayName = "Roles",
                Resources = { }
            });
    
            // MerkeziSSO scope (for accessing SSO's own APIs)
            await CreateScopesAsync(new OpenIddictScopeDescriptor
            {
                Name = "MerkeziSSO",
                DisplayName = "Merkezi SSO API",
                Resources =
                {
                    "MerkeziSSO"
                }
            });
        }
    
        private async Task CreateClientsAsync()
        {
            var commonScopes = new List<string>
            {
                OpenIddictConstants.Permissions.Scopes.Email,
                OpenIddictConstants.Permissions.Scopes.Profile,
                OpenIddictConstants.Permissions.Scopes.Roles,
                OpenIddictConstants.Permissions.Prefixes.Scope + "MerkeziSSO",
                OpenIddictConstants.Permissions.Prefixes.Scope + "TestClient",  // Important!
                OpenIddictConstants.Permissions.Scopes.OfflineAccess
            };
    
            // Client for your Blazor/React App
            await CreateOrUpdateApplicationAsync(
                applicationType: OpenIddictConstants.ApplicationTypes.Web,
                name: "TestClient_App",
                type: OpenIddictConstants.ClientTypes.Confidential,
                consentType: OpenIddictConstants.ConsentTypes.Implicit,
                displayName: "Test Client Application",
                secret: Configuration["TestClient_App:ClientSecret"] ?? "1q2w3E*",
                grantTypes: new List<string>
                {
                    OpenIddictConstants.GrantTypes.AuthorizationCode,
                    OpenIddictConstants.GrantTypes.RefreshToken
                },
                scopes: commonScopes,
                redirectUris: new List<string>
                {
                    "https://localhost:44305/signin-oidc",  // Your Blazor app URL
                    "https://localhost:44305"
                },
                postLogoutRedirectUris: new List<string>
                {
                    "https://localhost:44305/signout-callback-oidc",
                    "https://localhost:44305"
                },
                clientUri: "https://localhost:44305"
            );
    
            // Swagger client for API testing
            await CreateOrUpdateApplicationAsync(
                applicationType: OpenIddictConstants.ApplicationTypes.Web,
                name: "TestClient_Swagger",
                type: OpenIddictConstants.ClientTypes.Confidential,
                consentType: OpenIddictConstants.ConsentTypes.Implicit,
                displayName: "Test Client Swagger",
                secret: Configuration["TestClient_Swagger:ClientSecret"] ?? "1q2w3E*",
                grantTypes: new List<string>
                {
                    OpenIddictConstants.GrantTypes.AuthorizationCode
                },
                scopes: commonScopes,
                redirectUris: new List<string>
                {
                    "https://localhost:44308/swagger/oauth2-redirect.html"  // Your API Host URL
                },
                clientUri: "https://localhost:44308"
            );
        }
    }
    

    Step 2: Configure API Host (TestClient.HttpApi.Host)

    2.1 Update appsettings.json

    {
      "App": {
        "SelfUrl": "https://localhost:44308",
        "CorsOrigins": "https://*.TestClient.com,https://localhost:44305,https://localhost:44387"
      },
      "ConnectionStrings": {
        "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=TestClient;Trusted_Connection=True;TrustServerCertificate=True"
      },
      "AuthServer": {
        "Authority": "https://localhost:44387",  // Your MerkeziSSO Auth Server URL
        "RequireHttpsMetadata": true,
        "Audience": "TestClient",  // Must match API resource/scope name
        "SwaggerClientId": "TestClient_Swagger",
        "SwaggerClientSecret": "1q2w3E*"
      },
      "StringEncryption": {
        "DefaultPassPhrase": "SAME_AS_SSO_PASSPHRASE"  // CRITICAL: Must match SSO exactly!
      },
      "Redis": {
        "Configuration": "127.0.0.1"
      }
    }
    

    2.2 Update Module Configuration

    Ensure your TestClientHttpApiHostModule.cs is configured properly:

    private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
    {
        context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddAbpJwtBearer(options =>
            {
                options.Authority = configuration["AuthServer:Authority"];
                options.RequireHttpsMetadata = configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata");
                options.Audience = configuration["AuthServer:Audience"] ?? "TestClient";
                
                // Token validation parameters
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidAudiences = new[] { "TestClient" },
                    ClockSkew = TimeSpan.Zero
                };
            });
    }
    
    private void ConfigureSwagger(ServiceConfigurationContext context, IConfiguration configuration)
    {
        context.Services.AddAbpSwaggerGenWithOAuth(
            configuration["AuthServer:Authority"],
            new Dictionary<string, string>
            {
                {"TestClient", "TestClient API"}  // API scope
            },
            options =>
            {
                options.SwaggerDoc("v1", new OpenApiInfo { Title = "TestClient API", Version = "v1" });
                options.DocInclusionPredicate((docName, description) => true);
                options.CustomSchemaIds(type => type.FullName);
            });
    }
    

    Step 3: Configure Blazor/React Client Application

    3.1 Update appsettings.json (Blazor)

    {
      "App": {
        "SelfUrl": "https://localhost:44305"
      },
      "AuthServer": {
        "Authority": "https://localhost:44387",  // Your MerkeziSSO Auth Server
        "ClientId": "TestClient_App",
        "ClientSecret": "1q2w3E*",
        "RequireHttpsMetadata": true,
        "ResponseType": "code",
        "Scope": "openid profile email phone roles MerkeziSSO TestClient offline_access"  // Include TestClient scope!
      },
      "RemoteServices": {
        "Default": {
          "BaseUrl": "https://localhost:44308/"  // Your API Host URL
        }
      },
      "StringEncryption": {
        "DefaultPassPhrase": "SAME_AS_SSO_PASSPHRASE"  // CRITICAL: Must match SSO exactly!
      }
    }
    

    3.2 Authentication Configuration (Blazor Module)

    private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
    {
        context.Services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies", options =>
            {
                options.ExpireTimeSpan = TimeSpan.FromDays(365);
                options.CheckTokenExpiration();
            })
            .AddAbpOpenIdConnect("oidc", options =>
            {
                options.Authority = configuration["AuthServer:Authority"];
                options.RequireHttpsMetadata = configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata");
                options.ResponseType = OpenIdConnectResponseType.Code;  // Authorization Code flow
    
                options.ClientId = configuration["AuthServer:ClientId"];
                options.ClientSecret = configuration["AuthServer:ClientSecret"];
    
                options.UsePkce = true;
                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
    
                // Add required scopes
                options.Scope.Clear();
                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("email");
                options.Scope.Add("roles");
                options.Scope.Add("MerkeziSSO");  // SSO API scope
                options.Scope.Add("TestClient");  // Your API scope - CRITICAL!
                options.Scope.Add("offline_access");
            });
    }
    

    Step 4: Solve the 403 Forbidden Issue

    The 403 error occurs because permissions are stored in the database, and your API application uses a different database than the SSO server. There are three solutions:

    Solution A: Share the Same Database (Recommended for Monolith)

    Configure both MerkeziSSO and TestClient.HttpApi.Host to use the same database connection string:

    "ConnectionStrings": {
      "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MerkeziSSO;Trusted_Connection=True;TrustServerCertificate=True"
    }
    

    Pros:

    • Simplest solution
    • User permissions automatically available
    • No data synchronization needed

    Cons:

    • Tight coupling between applications
    • Not suitable for true microservices

    Solution B: Synchronize User and Permission Data

    Create a data synchronization mechanism:

    1. When a user logs in or permissions change in SSO, sync to TestClient database
    2. Use Distributed Events or message queues (RabbitMQ, Kafka)
    3. Listen to user/permission change events and replicate data

    Example using ABP Distributed Events:

    // In MerkeziSSO - Publish event when permissions change
    public class PermissionChangeNotifier
    {
        private readonly IDistributedEventBus _distributedEventBus;
    
        public PermissionChangeNotifier(IDistributedEventBus distributedEventBus)
        {
            _distributedEventBus = distributedEventBus;
        }
    
        public async Task NotifyPermissionChange(Guid userId, string[] permissions)
        {
            await _distributedEventBus.PublishAsync(new UserPermissionsChangedEto
            {
                UserId = userId,
                Permissions = permissions
            });
        }
    }
    
    // In TestClient - Handle event and update local database
    public class UserPermissionsChangedEventHandler :
        IDistributedEventHandler<UserPermissionsChangedEto>,
        ITransientDependency
    {
        private readonly IPermissionManager _permissionManager;
    
        public UserPermissionsChangedEventHandler(IPermissionManager permissionManager)
        {
            _permissionManager = permissionManager;
        }
    
        public async Task HandleEventAsync(UserPermissionsChangedEto eventData)
        {
            // Update permissions in local database
            foreach (var permission in eventData.Permissions)
            {
                await _permissionManager.SetForUserAsync(
                    eventData.UserId,
                    permission,
                    true
                );
            }
        }
    }
    

    Solution C: Include Permissions in JWT Claims (Recommended for Microservices)

    Modify the central auth server to include permissions as claims in the JWT token:

    // In MerkeziSSO - Add custom claims contributor
    public class PermissionClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
    {
        private readonly IPermissionManager _permissionManager;
        private readonly IPermissionDefinitionManager _permissionDefinitionManager;
    
        public PermissionClaimsPrincipalContributor(
            IPermissionManager permissionManager,
            IPermissionDefinitionManager permissionDefinitionManager)
        {
            _permissionManager = permissionManager;
            _permissionDefinitionManager = permissionDefinitionManager;
        }
    
        public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
        {
            var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
            var userId = identity?.FindUserId();
            
            if (userId.HasValue)
            {
                // Get all permissions for user
                var permissions = await _permissionManager.GetAllForUserAsync(userId.Value);
                
                // Add permissions as claims
                foreach (var permission in permissions)
                {
                    if (permission.IsGranted)
                    {
                        identity.AddClaim(new Claim("permission", permission.Name));
                    }
                }
            }
        }
    }
    
    // In TestClient API - Create custom permission value provider
    public class ClaimPermissionValueProvider : PermissionValueProvider
    {
        public ClaimPermissionValueProvider(IPermissionStore permissionStore)
            : base(permissionStore)
        {
        }
    
        public override string Name => "Claim";
    
        public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
        {
            var permissionClaims = context.Principal?
                .FindAll("permission")
                .Select(c => c.Value)
                .ToList();
    
            if (permissionClaims != null && permissionClaims.Contains(context.Permission.Name))
            {
                return PermissionGrantResult.Granted;
            }
    
            return PermissionGrantResult.Undefined;
        }
    }
    
    // Register the custom provider in TestClientHttpApiHostModule
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // ... other configurations ...
        
        Configure<AbpPermissionOptions>(options =>
        {
            options.ValueProviders.Add<ClaimPermissionValueProvider>();
        });
    }
    

    Don't forget to configure OpenIddict to include the permission claims:

    // In MerkeziSSO - Configure OpenIddict claims
    Configure<AbpClaimsServiceOptions>(options =>
    {
        options.RequestedClaims.AddRange(new[] { "permission" });
    });
    
  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Step 5: Update RedirectAllowedUrls in Auth Server

    In MerkeziSSO.AuthServer project's appsettings.json:

    {
      "App": {
        "SelfUrl": "https://localhost:44387",
        "RedirectAllowedUrls": "https://localhost:44305,https://localhost:44308"  // Client and API URLs
      }
    }
    

    Step 6: Testing with Postman

    Request Token

    POST https://localhost:44387/connect/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
    client_id=TestClient_Swagger
    client_secret=1q2w3E*
    code=<authorization_code>
    redirect_uri=https://localhost:44308/swagger/oauth2-redirect.html
    
    # OR for password grant (if enabled for testing)
    grant_type=password
    client_id=TestClient_Swagger
    client_secret=1q2w3E*
    username=admin
    password=1q2w3E*
    scope=openid profile email roles MerkeziSSO TestClient offline_access
    

    CRITICAL: The scope parameter MUST include TestClient (your API resource scope).

    Call API with Token

    POST https://localhost:44308/api/identity/users
    Authorization: Bearer <access_token>
    Content-Type: application/json
    
    {
      "userName": "testuser",
      "name": "Test",
      "surname": "User",
      "email": "test@test.com",
      "password": "1q2w3E*",
      "roleNames": ["admin"]
    }
    

    Step 7: Verify Token Contents

    Decode your JWT token at jwt.io and verify:

    1. aud (audience) claim contains TestClient
    2. scope claim contains TestClient
    3. sub (subject) contains user ID
    4. If using Solution C, permission claims are present

    Example token payload:

    {
      "iss": "https://localhost:44387",
      "aud": ["TestClient", "MerkeziSSO"],
      "sub": "39f69c84-2862-bcba-ca19-3a0dbbbfe559",
      "email": "admin@abp.io",
      "role": "admin",
      "scope": ["openid", "profile", "email", "roles", "TestClient", "MerkeziSSO", "offline_access"],
      "permission": ["AbpIdentity.Users", "AbpIdentity.Users.Create"],  // If using Solution C
      "exp": 1729565400,
      "iat": 1729561800
    }
    

    Troubleshooting Checklist

    • [ ] String Encryption PassPhrase is EXACTLY the same in all three applications (SSO, API, Client)
    • [ ] Redis cache is cleared after configuration changes
    • [ ] Database is seeded from scratch with new OpenIddict applications
    • [ ] Scope "TestClient" is created in OpenIddict scopes table
    • [ ] Client applications have permission to request "TestClient" scope
    • [ ] Token request includes "TestClient" scope
    • [ ] API Host Audience configuration matches "TestClient"
    • [ ] User exists in the database that API application is using (if using Solution A or B)
    • [ ] Permissions are granted to the user in the database (if using Solution A or B)
    • [ ] Permission claims are in the token (if using Solution C)
    • [ ] CORS is configured to allow your client URLs
    • [ ] RedirectAllowedUrls includes client and API URLs

    Common Errors and Solutions

    Error: "invalid_scope"

    Cause: The requested scope doesn't exist in OpenIddict scopes. Solution: Ensure "TestClient" scope is created and the client has permission to request it.

    Error: 401 Unauthorized with "invalid_token"

    Cause: Token validation failed - audience mismatch or wrong authority. Solution: Verify Audience in API appsettings matches the scope name, and Authority points to SSO.

    Error: 403 Forbidden

    Cause: User authenticated but lacks permissions. Solutions:

    1. Use shared database (Solution A)
    2. Synchronize permissions to API database (Solution B)
    3. Include permissions in JWT claims (Solution C)

    Error: "String encryption error"

    Cause: Different StringEncryption:DefaultPassPhrase across applications. Solution: Copy the exact passphrase from SSO to all applications.

    Architecture Recommendations

    For Monolith or Closely Coupled Apps

    • Use Solution A (shared database)
    • Simplest to implement and maintain
    • All applications access same user and permission data

    For Microservices Architecture

    • Use Solution C (permissions in claims)
    • Each service is independent
    • No database coupling
    • Tokens can be larger (consider token size limits)
    • Best for distributed systems

    Hybrid Approach

    • Critical permissions in claims
    • Detailed permissions from database
    • Use caching to minimize database queries
    • Sync only essential user/permission data

    The 403 Forbidden error specifically requires addressing the permission storage issue using one of the three solutions provided above.

  • User Avatar
    0
    cetin.sahin created

    Hello; I received the following errors in the first step of your steps. Unfortunately, I couldn't proceed to the next steps. Does your suggested solution work on your local computer?

  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Hi,

    // ... other code ...
    

    indicates other unimportant code blocks that is obvious what they do. If you need a key solution, I can share the exact entire class with you.

    Here what you need to do:

    File: MerkeziSSO.DbMigrator/appsettings.json

    Add this configuration section (merge with existing config):

    {
      "ConnectionStrings": {
        "Default": "..."
      },
      "OpenIddict": {
        "Applications": {
          "TestClient_App": {
            "ClientId": "TestClient_App",
            "ClientSecret": "1q2w3E*",
            "RootUrl": "https://localhost:44305"
          },
          "TestClient_Swagger": {
            "ClientId": "TestClient_Swagger",
            "RootUrl": "https://localhost:44308"
          }
        }
      }
    }
    

    The entire file of OpenIddictDataSeedContributor.cs

    public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDependency
    {
        private readonly IConfiguration _configuration;
        private readonly IOpenIddictApplicationRepository _openIddictApplicationRepository;
        private readonly IAbpApplicationManager _applicationManager;
        private readonly IOpenIddictScopeRepository _openIddictScopeRepository;
        private readonly IOpenIddictScopeManager _scopeManager;
    
        public OpenIddictDataSeedContributor(
            IConfiguration configuration,
            IOpenIddictApplicationRepository openIddictApplicationRepository,
            IAbpApplicationManager applicationManager,
            IOpenIddictScopeRepository openIddictScopeRepository,
            IOpenIddictScopeManager scopeManager)
        {
            _configuration = configuration;
            _openIddictApplicationRepository = openIddictApplicationRepository;
            _applicationManager = applicationManager;
            _openIddictScopeRepository = openIddictScopeRepository;
            _scopeManager = scopeManager;
        }
    
        public async Task SeedAsync(DataSeedContext context)
        {
            await CreateApiScopesAsync();
            await CreateClientsAsync();
        }
    
        private async Task CreateApiScopesAsync()
        {
            // Create a scope for your API application
            if (await _openIddictScopeRepository.FindByNameAsync("TestClient") == null)
            {
                await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
                {
                    Name = "TestClient",  // This is your API resource scope
                    DisplayName = "Test Client API",
                    Description = "Test Client API Scope",
                    Resources = { "TestClient" }  // API resource name
                });
            }
    
            // MerkeziSSO scope (for accessing SSO's own APIs)
            if (await _openIddictScopeRepository.FindByNameAsync("MerkeziSSO") == null)
            {
                await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
                {
                    Name = "MerkeziSSO",
                    DisplayName = "Merkezi SSO API",
                    Resources = { "MerkeziSSO" }
                });
            }
        }
    
        private async Task CreateClientsAsync()
        {
            var commonScopes = new List<string>
            {
                OpenIddictConstants.Permissions.Scopes.Address,
                OpenIddictConstants.Permissions.Scopes.Email,
                OpenIddictConstants.Permissions.Scopes.Phone,
                OpenIddictConstants.Permissions.Scopes.Profile,
                OpenIddictConstants.Permissions.Scopes.Roles,
                "MerkeziSSO",  // Custom scopes are just strings
                "TestClient"   // Important! Your API scope
            };
    
            // Client for your Blazor/React App
            var webClientId = _configuration["OpenIddict:Applications:TestClient_App:ClientId"];
            if (!string.IsNullOrWhiteSpace(webClientId))
            {
                var webClientRootUrl = _configuration["OpenIddict:Applications:TestClient_App:RootUrl"]!.TrimEnd('/');
                
                await CreateApplicationAsync(
                    name: webClientId!,
                    type: OpenIddictConstants.ClientTypes.Confidential,
                    consentType: OpenIddictConstants.ConsentTypes.Implicit,
                    displayName: "Test Client Application",
                    secret: _configuration["OpenIddict:Applications:TestClient_App:ClientSecret"] ?? "1q2w3E*",
                    grantTypes: new List<string>
                    {
                        OpenIddictConstants.GrantTypes.AuthorizationCode,
                        OpenIddictConstants.GrantTypes.RefreshToken
                    },
                    scopes: commonScopes,
                    clientUri: webClientRootUrl,
                    redirectUri: $"{webClientRootUrl}/signin-oidc",
                    postLogoutRedirectUri: $"{webClientRootUrl}/signout-callback-oidc"
                );
            }
    
            // Swagger client for API testing
            var swaggerClientId = _configuration["OpenIddict:Applications:TestClient_Swagger:ClientId"];
            if (!string.IsNullOrWhiteSpace(swaggerClientId))
            {
                var swaggerRootUrl = _configuration["OpenIddict:Applications:TestClient_Swagger:RootUrl"]?.TrimEnd('/');
                
                await CreateApplicationAsync(
                    name: swaggerClientId!,
                    type: OpenIddictConstants.ClientTypes.Public,
                    consentType: OpenIddictConstants.ConsentTypes.Implicit,
                    displayName: "Test Client Swagger",
                    secret: null,
                    grantTypes: new List<string>
                    {
                        OpenIddictConstants.GrantTypes.AuthorizationCode
                    },
                    scopes: commonScopes,
                    clientUri: swaggerRootUrl,
                    redirectUri: $"{swaggerRootUrl}/swagger/oauth2-redirect.html"
                );
            }
        }
    
        private async Task CreateApplicationAsync(
            string name,
            string type,
            string consentType,
            string displayName,
            string? secret,
            List<string> grantTypes,
            List<string> scopes,
            string? clientUri = null,
            string? redirectUri = null,
            string? postLogoutRedirectUri = null)
        {
            var client = await _openIddictApplicationRepository.FindByClientIdAsync(name);
    
            var application = new AbpApplicationDescriptor
            {
                ClientId = name,
                ClientType = type,
                ClientSecret = secret,
                ConsentType = consentType,
                DisplayName = displayName,
                ClientUri = clientUri,
            };
    
            // Add grant type permissions
            foreach (var grantType in grantTypes)
            {
                if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode)
                {
                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode);
                    application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
                }
    
                if (grantType == OpenIddictConstants.GrantTypes.RefreshToken)
                {
                    application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken);
                    application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
                }
    
                application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Revocation);
                application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
            }
    
            if (!string.IsNullOrWhiteSpace(redirectUri) || !string.IsNullOrWhiteSpace(postLogoutRedirectUri))
            {
                application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession);
            }
    
            // Add scope permissions
            var buildInScopes = new[]
            {
                OpenIddictConstants.Permissions.Scopes.Address,
                OpenIddictConstants.Permissions.Scopes.Email,
                OpenIddictConstants.Permissions.Scopes.Phone,
                OpenIddictConstants.Permissions.Scopes.Profile,
                OpenIddictConstants.Permissions.Scopes.Roles
            };
    
            foreach (var scope in scopes)
            {
                if (buildInScopes.Contains(scope))
                {
                    application.Permissions.Add(scope);
                }
                else
                {
                    application.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
                }
            }
    
            // Add redirect URIs
            if (!string.IsNullOrWhiteSpace(redirectUri))
            {
                if (Uri.TryCreate(redirectUri, UriKind.Absolute, out var uri))
                {
                    application.RedirectUris.Add(uri);
                }
            }
    
            if (!string.IsNullOrWhiteSpace(postLogoutRedirectUri))
            {
                if (Uri.TryCreate(postLogoutRedirectUri, UriKind.Absolute, out var uri))
                {
                    application.PostLogoutRedirectUris.Add(uri);
                }
            }
    
            if (client == null)
            {
                await _applicationManager.CreateAsync(application);
            }
            else
            {
                await _applicationManager.UpdateAsync(client.ToModel(), application);
            }
        }
    }
    

    It should be compiled without an error right know:


    If you go with Solution C:

    And also corrected the compilation errors in MerkeziSSO.Domain/Identity/PermissionClaimsPrincipalContributor.cs

    using System.Linq;
    using System.Security.Claims;
    using System.Security.Principal;
    using System.Threading.Tasks;
    using Volo.Abp.Authorization.Permissions;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.PermissionManagement;
    using Volo.Abp.Security.Claims;
    
    namespace MerkeziSSO.Identity
    {
        public class PermissionClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
        {
            private readonly IPermissionManager _permissionManager;
    
            public PermissionClaimsPrincipalContributor(IPermissionManager permissionManager)
            {
                _permissionManager = permissionManager;
            }
    
            public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
            {
                var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
                if (identity == null)
                {
                    return;
                }
    
                var userId = context.ClaimsPrincipal.FindUserId();
                
                if (userId.HasValue)
                {
                    // GetAllAsync with provider name and key
                    var permissions = await _permissionManager.GetAllAsync(
                        "U",  // User provider name (UserPermissionValueProvider.ProviderName)
                        userId.Value.ToString()
                    );
                    
                    //  Filter granted permissions and add as claims
                    foreach (var permission in permissions.Where(p => p.IsGranted))
                    {
                        identity.AddClaim(new Claim("permission", permission.Name));
                    }
                }
            }
        }
    }
    

    Also fixed version of TestClient.HttpApi.Host/Permissions/ClaimPermissionValueProvider.cs

    using System.Linq;
    using System.Threading.Tasks;
    using Volo.Abp.Authorization.Permissions;
    
    namespace TestClient.Permissions
    {
        public class ClaimPermissionValueProvider : PermissionValueProvider
        {
            public const string ProviderName = "Claim";
    
            public override string Name => ProviderName;
    
            public ClaimPermissionValueProvider(IPermissionStore permissionStore)
                : base(permissionStore)
            {
            }
    
            public override Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
            {
                var permissionClaims = context.Principal?
                    .FindAll("permission")
                    .Select(c => c.Value)
                    .ToList();
    
                if (permissionClaims != null && permissionClaims.Contains(context.Permission.Name))
                {
                    return Task.FromResult(PermissionGrantResult.Granted);
                }
    
                return Task.FromResult(PermissionGrantResult.Undefined);
            }
    
            public override Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context)
            {
                var permissionClaims = context.Principal?
                    .FindAll("permission")
                    .Select(c => c.Value)
                    .ToHashSet();
    
                var result = new MultiplePermissionGrantResult();
                
                if (permissionClaims != null)
                {
                    foreach (var permission in context.Permissions)
                    {
                        result.Result[permission.Name] = permissionClaims.Contains(permission.Name)
                            ? PermissionGrantResult.Granted
                            : PermissionGrantResult.Undefined;
                    }
                }
    
                return Task.FromResult(result);
            }
        }
    }
    

    And do not forget to register it in TestClientHttpApiHostModule.cs file:

    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // ... existing code ...
        
        // 👇 Add this configuration
        Configure<AbpPermissionOptions>(options =>
        {
            options.ValueProviders.Add<ClaimPermissionValueProvider>();
        });
    }
    

    Since your project using OpenIdDict module not IdentityServer, We'll need these extra steps:

    • MerkeziSSO.AuthServer/Identity/PermissionOpenIddictClaimsPrincipalHandler.cs
    public class PermissionOpenIddictClaimsPrincipalHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency
        {
            public Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
            {
                // Set destinations for permission claims
                foreach (var claim in context.Principal.Claims)
                {
                    if (claim.Type == "permission")
                    {
                        // Add permission claims to access token
                        claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
                    }
                }
    
                return Task.CompletedTask;
            }
        }
    
    • MerkeziSSOAuthServerModule.cs

    In ConfigureServices method, add this:

    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // ... your existing configuration ...
        
        // Register the OpenIddict claims handler
        Configure<AbpOpenIddictClaimsPrincipalOptions>(options =>
        {
            options.ClaimsPrincipalHandlers.Add<PermissionOpenIddictClaimsPrincipalHandler>();
        });
    }
    

    Example token payload:

    {
      "iss": "https://localhost:44387",
      "aud": ["TestClient"],
      "sub": "39f69c84-2862-bcba-ca19-3a0dbbbfe559",
      "email": "admin@abp.io",
      "role": "admin",
      "permission": [
        "AbpIdentity.Users",
        "AbpIdentity.Users.Create",
        "AbpIdentity.Users.Update"
      ],
      "exp": 1729565400
    }
    

    I personally do not suggest this way considering the token sizes is going to be huge. But it shows the approach. You may combine different approaches according to your requirements.


    I highly recommend to you to analyze the OpenIdDict module if you need to customize: https://github.com/abpframework/abp/tree/dev/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers

    The module and ABP's AuthServer template is designed to be used standalone Authentication Servers like Keycloak, Auth0, authentik etc... But the problem is user itself and ICurrentUser, and their permissions. If you're able to connect the same database at least for identity module it'll work.

    But same logic can be applied for PermissionManagement and SettingManagement module, too. If you want to manage permissions and setting globally across the system.

  • User Avatar
    0
    cetin.sahin created
    1. I corrected the part in step 1 with the code you provided. OK When I ran DBmigrator, clients like the ones below were created in CentralSSO.

    2. I see the ConnectionString is the default in the TestClient API host project. Don't we need to separate the connection strings here? In our scenario, the Auth server and the test client will run on two different databases.

    "AbpIdentityServer": "Server=localhost;Database=MyIdsDb;Trusted_Connection=True;",

    "AbpPermissionManagement": "Server=localhost;Database=MyPermissionDb;Trusted_Connection=True;"

    1. I get an error when I add the following code to the MerkeziSSO.AuthServer project in the MerkeziSSO project.

    1. I get an error when I add the following code to the TestClient.HttpApi.Host project in the TestClient project.

    Can you share the two projects (CentralSSO, TestClient) and the Postman collection running on your local machine?

  • User Avatar
    0
    cetin.sahin created

    Any update ?

  • User Avatar
    0
    cetin.sahin created

    Any update ?

  • User Avatar
    0
    cetin.sahin created

    Any update ?

Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.1.0-preview. Updated on October 27, 2025, 08:34