421.7 Abp microserices and Keycloak - chempkovsky/CS82ANGULAR GitHub Wiki

Notes

  • Only security aspects will be discussed and implemented.
  • In this article we will setup angular app

Tools

environment file

  • modify rupbes.tstapp\angular\projects\dev-app\src\environments\environment.ts-file as follows
Click to show the code
import { Environment } from '@abp/ng.core';

const baseUrl = 'http://localhost:4200';

export const environment = {
  production: false,
  application: {
    baseUrl: 'http://localhost:4200/',
    name: 'tstapp_angular',
    logoUrl: '',
  },
  oAuthConfig: {
    issuer: 'https://kc.rupbes.by:8445/realms/rupbes.tstrealm',
    redirectUri: baseUrl,
    clientId: 'tstapp_angular',
    responseType: 'code',
    scope: 'offline_access tstapihost_scope tstauth_scope tenantid roles email phone',
    requireHttps: true
  },
  apis: {
    default: {
      url: 'https://localhost:54635/',
      rootNamespace: 'rupbes.tstapp',
    },
    tstapp: {
      url: 'https://localhost:44323',
      rootNamespace: 'rupbes.tstapp',
    },
  },
} as Environment;

tstapp_angular

  • We have to set Client authentication: OFF

  • In keycloak console

    • click Manage realms-menu
    • click rupbes.tstrealm
    • click Clients-menu
    • click Create Client-button
  • On the Create client-page

    • Client type: OpenID Connect
    • Client ID: tstapp_angular
    • Client authentication: OFF
    • Authorization: OFF
    • Authentication flow: Standard flow
    • Root URL: http://localhost:4200 (we copied this value from environment/application/baseUrl-prop above)
    • Home URL: http://localhost:4200 (we copied this value from environment/application/baseUrl-prop above)
    • Web origins: http://localhost:4200 (we copied this value from environment/application/baseUrl-prop above)
    • Valid redirect URIs: http://localhost:4200
    • Valid post logout redirect URIs: http://localhost:4200
    • click Save-button
  • Add the scopes below to tstapp_angular-app:

  • Add admin role

    • assign this admin-role to the tstadmin-user

Launch apps

  • run rupbes.tstapp.Auth.Host-app. It will create tstadmin-user
  • run angular-app (do not stop rupbes.tstapp.Auth.Host-app)
    • in the folder rupbes.tstapp\angular execute the command: ng serve -o
  • login as tstadmin-user and password that was defined in keycloak (you can redefine password in keycloak for tstadmin-user. It must be different from 1q2w3E* for correct testing)

Security important note

  • tenantid scope must be defined as Default (not as Optional).
  • If tenantid-scope is optional:
    • create a tenant in Abp-app
    • in keycloak console create tenant user
    • in keycloak console assign Admin-role to tenant user. We obtain tenant admin
    • Modify scope: 'offline_access tstapihost_scope tstauth_scope roles email phone' in environment.ts-file (remove tenantid from the list)
    • login as tenant admin in abp-app and you will get Host Admin Rights instead of Tenant Admin Rights

Revoke errors

  • We have no errors for the host user (tenantid==null), but for the tenant user (tenantid != null) angular application throws exception during 'log out':
Access to XMLHttpRequest at 'https://kc.rupbes.by:8445/realms/rupbes.tstrealm/.well-known/openid-configuration' from origin 'http://localhost:4200' has been blocked by CORS policy: Request header field __tenant is not allowed by Access-Control-Allow-Headers in preflight response.
  • We tried to rewrite OAuthApiInterceptor (it is Abp Interceptor) but to no avail. Refreshing browser page throws an exception.
Click to show the code
    // providers: [
    // ...
    // {
    //     provide: HTTP_INTERCEPTORS,
    //     useExisting:  CustomOAuthApiInterceptor,
    //     multi: true
    // },
    // { provide: OAuthApiInterceptor, useClass: CustomOAuthApiInterceptor } 
    // ...
    // ]
    //

import { HttpEvent, HttpHandler, HttpHeaders, HttpRequest } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { finalize } from 'rxjs/operators';
import { Observable } from 'rxjs';
import {
  HttpWaitService,
  IApiInterceptor,
  IS_EXTERNAL_REQUEST,
  SessionStateService,
  TENANT_KEY,
} from '@abp/ng.core';
import { OAuthApiInterceptor } from '@abp/ng.oauth';

@Injectable({
  providedIn: 'root',
})
export class CustomOAuthApiInterceptor implements IApiInterceptor {

  protected oAuthService = inject(OAuthService);
  protected sessionState = inject(SessionStateService);
  protected httpWaitService = inject(HttpWaitService);
  protected tenantKey = inject(TENANT_KEY);

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.httpWaitService.addRequest(request);
    const isExternalRequest = request.context?.get(IS_EXTERNAL_REQUEST);
    // const newRequest = isExternalRequest
    //   ? request
    //   : request.clone({
    //       setHeaders: this.getAdditionalHeaders(request.headers),
    //     });
    const newRequest = request.clone({
           setHeaders: this.getAdditionalHeaders(request.headers),
         });

    return next
      .handle(newRequest)
      .pipe(finalize(() => this.httpWaitService.deleteRequest(request)));
  }


  getAdditionalHeaders(existingHeaders?: HttpHeaders) {
    const headers = {} as any;

    const token = this.oAuthService.getAccessToken();
    if (!existingHeaders?.has('Authorization') && token) {
      headers['Authorization'] = `Bearer ${token}`;
    }

    const lang = this.sessionState.getLanguage();
    if (!existingHeaders?.has('Accept-Language') && lang) {
      headers['Accept-Language'] = lang;
    }

    this.sessionState.setTenant(null);
    // console.log('this.sessionState.getTenant()=', this.sessionState.getTenant());
    
    //const tenant = this.sessionState.getTenant();
    //if (!existingHeaders?.has(this.tenantKey) && tenant?.id) {
    //  headers[this.tenantKey] = tenant.id;
    //}
    

    headers['X-Requested-With'] = 'XMLHttpRequest';

    console.log('headers=', headers);
    return headers;
  }
}
  • we didn't waste time solving the problem with Keycloak
  • Instead, we rewrote the AbpApplicationConfigurationAppService class
    • In the rupbes.tstapp.Auth.Host.csproj-project create ConfigurationService-folder
    • In the ConfigurationService-folder create CustomAbpApplicationConfigurationAppService-class as follows:
Click to show the code
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using System;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending;
using Volo.Abp.AspNetCore.Mvc.MultiTenancy;
using Volo.Abp.Authorization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
using Volo.Abp.Timing;
using Volo.Abp.Users;
using Volo.Abp.DependencyInjection;

namespace rupbes.tstapp.ConfigurationService
{
    [Dependency(ReplaceServices = true)]
    public class CustomAbpApplicationConfigurationAppService: AbpApplicationConfigurationAppService
    {
        public CustomAbpApplicationConfigurationAppService(IOptions<AbpLocalizationOptions> localizationOptions, IOptions<AbpMultiTenancyOptions> multiTenancyOptions, IServiceProvider serviceProvider, IAbpAuthorizationPolicyProvider abpAuthorizationPolicyProvider, IPermissionDefinitionManager permissionDefinitionManager, DefaultAuthorizationPolicyProvider defaultAuthorizationPolicyProvider, IPermissionChecker permissionChecker, IAuthorizationService authorizationService, ICurrentUser currentUser, ISettingProvider settingProvider, ISettingDefinitionManager settingDefinitionManager, IFeatureDefinitionManager featureDefinitionManager, ILanguageProvider languageProvider, ITimezoneProvider timezoneProvider, IOptions<AbpClockOptions> abpClockOptions, ICachedObjectExtensionsDtoService cachedObjectExtensionsDtoService, IOptions<AbpApplicationConfigurationOptions> options) : base(localizationOptions, multiTenancyOptions, serviceProvider, abpAuthorizationPolicyProvider, permissionDefinitionManager, defaultAuthorizationPolicyProvider, permissionChecker, authorizationService, currentUser, settingProvider, settingDefinitionManager, featureDefinitionManager, languageProvider, timezoneProvider, abpClockOptions, cachedObjectExtensionsDtoService, options)
        {
        }

        protected override CurrentTenantDto GetCurrentTenant()
        {
            return new CurrentTenantDto()
            {
                Id = null, // CurrentTenant.Id,
                Name = null, // CurrentTenant.Name!,
                IsAvailable = false //CurrentTenant.IsAvailable
            };
        }
    }
}
  • So,
    • Angular application knows nothing about the tenant of the current user.
      • But it work as expected.
    • Back-end apps work as expected as well (rupbes.tstapp.Auth.Host and rupbes.tstapp.HttpApi.Host).
      • Backend applications have information about the current user's tenant, and that's more than enough.
    • The MVC application also works as expected (rupbes.tstapp.Web.Host) even without knowing about the tenant of the current user.
  • It'll be a prototype pattern for production project.

keycloak pkce and groups

  • important
⚠️ **GitHub.com Fallback** ⚠️