Codeunit 6300 Azure AD Mgt.

App
Base Application
Namespace
System.Azure.Identity
Versions
17-28

Procedures, 22Obsolete, 5

Versions171819202122232425262728

Source29

Source in 29

src/Layers/W1/BaseApp/Modules/System/AzureADAuthentication/AzureADMgt.Codeunit.al310 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.Azure.Identity;

using System;
using System.Environment;
using System.Utilities;

codeunit 6300 "Azure AD Mgt."
{
    // // Provides functions to authorize NAV app to use Azure Active Directory resources on behalf of a user.
    InherentPermissions = X;
    InherentEntitlements = X;


    trigger OnRun()
    begin
    end;

    var
        AzureADAppSetup: Record "Azure AD App Setup";
        AzureADAuthFlow: Codeunit "Azure AD Auth Flow";
        AzureADNotSetupErr: Label '%1 is not registered in your Microsoft Entra tenant.', Comment = '%1 - product name';
        O365ResourceNameTxt: Label 'Office 365 Services', Locked = true;
        OAuthLandingPageTxt: Label 'OAuthLanding.htm', Locked = true;

    [Scope('OnPrem')]
    procedure GetAuthCodeUrl(ResourceName: Text) AuthCodeUrl: Text
    begin
        // Pass ResourceName as empty string if you want to authorize all azure resources.
        AuthCodeUrl := GetAzureADAuthEndpoint();
        AuthCodeUrl += '?response_type=code';
        AuthCodeUrl += '&client_id=' + UrlEncode(GetClientId());
        if ResourceName <> '' then
            AuthCodeUrl += '&resource=' + UrlEncode(ResourceName);
        AuthCodeUrl += '&redirect_uri=' + UrlEncode(GetRedirectUrl());
    end;

    [Scope('OnPrem')]
    procedure AcquireTokenByAuthorizationCodeAsSecretText(AuthorizationCode: SecretText; ResourceUrl: Text) AccessToken: SecretText
    begin
        // This will return access token and also cache it for future use.
        AzureADAuthFlow.Initialize(GetRedirectUrl());

        if IsSaaS() then
            AccessToken := AzureADAuthFlow.AcquireTokenByAuthorizationCodeAsSecretText(AuthorizationCode, ResourceUrl)
        else begin
            if not AzureADAppSetup.FindFirst() then
                exit;
            AccessToken := AzureADAuthFlow.AcquireTokenByAuthorizationCodeWithCredentialsAsSecretText(
                AuthorizationCode,
                GetClientId(),
                AzureADAppSetup.GetSecretKeyFromIsolatedStorageAsSecretText(),
                ResourceUrl);
        end;
    end;

    [Scope('OnPrem')]
    procedure GetAccessTokenAsSecretText(ResourceUrl: Text; ResourceName: Text; ShowDialog: Boolean) AccessToken: SecretText
    var
        AzureADAccessDialog: Page "Azure AD Access Dialog";
        AuthorizationCode: SecretText;
    begin
        // Does everything required to retrieve an access token for the given service, including
        // showing the Azure AD wizard and auth code retrieval form if necessary.
        if (not IsAzureADAppSetupDone()) and ShowDialog then begin
            PAGE.RunModal(PAGE::"Azure AD App Setup Wizard");
            if not IsAzureADAppSetupDone() then
                // Don't continue if user cancelled or errored out of the setup wizard.
                exit(AccessToken);
        end;

        if AcquireToken(ResourceUrl, AccessToken) then
            if not AccessToken.IsEmpty() then
                exit(AccessToken);

        if IsSaaS() then begin
            Clear(AccessToken);
            exit(AccessToken);
        end;

        if ShowDialog then
            AuthorizationCode := AzureADAccessDialog.GetAuthorizationCodeAsSecretText(ResourceUrl, ResourceName);
        if not AuthorizationCode.IsEmpty() then
            AccessToken := AcquireTokenByAuthorizationCodeAsSecretText(AuthorizationCode, ResourceUrl);
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure GetGuestAccessToken(ResourceUrl: Text; GuestTenantId: Text) AccessToken: Text
    begin
        // Gets an access token for a guest user on a different tenant
        if AcquireGuestToken(ResourceUrl, GuestTenantId, AccessToken) then
            if AccessToken <> '' then
                exit(AccessToken);
    end;

    [Scope('OnPrem')]
    procedure GetOnBehalfAccessTokenAsSecretText(ResourceUrl: Text): SecretText
    begin
        AzureADAuthFlow.Initialize(GetRedirectUrl());
        exit(AzureADAuthFlow.AcquireOnBehalfOfTokenAsSecretText(ResourceUrl));
    end;

    [NonDebuggable]
    [Scope('OnPrem')]
    procedure GetOnBehalfAccessTokenAndTokenCacheState(ResourceUrl: Text; var TokenCacheState: Text): Text
    begin
        AzureADAuthFlow.Initialize(GetRedirectUrl());
        exit(AzureADAuthFlow.AcquireOnBehalfOfTokenAndTokenCacheState(ResourceUrl, TokenCacheState));
    end;

    [NonDebuggable]
    [Scope('OnPrem')]
    procedure GetTokenFromTokenCacheState(ResourceId: Text; AadUserId: Text; TokenCacheState: Text; var NewTokenCacheState: Text): Text
    begin
        AzureADAuthFlow.Initialize(GetRedirectUrl());
        exit(AzureADAuthFlow.AcquireTokenFromCacheState(ResourceId, AadUserId, TokenCacheState, NewTokenCacheState));
    end;

    local procedure UrlEncode(UrlComponent: Text): Text
    var
        HttpUtility: DotNet HttpUtility;
    begin
        exit(HttpUtility.UrlEncode(UrlComponent));
    end;

    procedure GetAzureADAuthEndpoint(): Text
    var
        UrlHelper: Codeunit "Url Helper";
    begin
        exit(UrlHelper.GetAzureADAuthEndpoint());
    end;

    [Scope('OnPrem')]
    procedure GetDefaultRedirectUrl(): Text[150]
    var
        UriBuilder: DotNet UriBuilder;
        PathString: DotNet String;
        RedirectUrl: Text;
    begin
        // Retrieve the Client URL
        RedirectUrl := GetUrl(ClientType::Web);
        // For SaaS Extract the Base Url (domain) from the full CLient URL
        if IsSaaS() then
            RedirectUrl := GetBaseUrl(RedirectUrl);

        // Due to a bug in ADAL 2.9, it will not consider URI's to be equal if one URI specified the default port number (ex: 443 for HTTPS)
        // and the other did not. UriBuilder(...).Uri.ToString() is a way to remove any protocol-default port numbers, such as 80 for HTTP
        // and 443 for HTTPS. This bug appears to be fixed in ADAL 3.1+.
        UriBuilder := UriBuilder.UriBuilder(RedirectUrl);

        // Append a '/' character to the end of the path if one does not exist already.
        PathString := UriBuilder.Path;
        if PathString.LastIndexOf('/') < (PathString.Length - 1) then
            UriBuilder.Path := UriBuilder.Path + '/';

        // Append the desired redirect page to the path.
        UriBuilder.Path := UriBuilder.Path + OAuthLandingPageTxt;
        UriBuilder.Query := '';

        // Pull out the full URL by the URI and convert it to a string.
        RedirectUrl := UriBuilder.Uri.ToString();

        exit(CopyStr(RedirectUrl, 1, 150));
    end;

    [Scope('OnPrem')]
    procedure GetRedirectUrl(): Text[150]
    begin
        if not IsSaaS() and not AzureADAppSetup.IsEmpty() then begin
            // Use existing redirect URL if already in table - necessary for Windows client which would otherwise
            // generate a different URL for each computer and thus not match the company's Azure application.
            AzureADAppSetup.FindFirst();
            exit(AzureADAppSetup."Redirect URL");
        end;

        exit(GetDefaultRedirectUrl());
    end;

    local procedure GetBaseUrl(RedirectUrl: Text): Text
    var

        BaseIndex: Integer;
        EndBaseUrlIndex: Integer;
        Baseurl: Text;
    begin
        if StrPos(LowerCase(RedirectUrl), 'https://') <> 0 then
            BaseIndex := 9;
        if StrPos(LowerCase(RedirectUrl), 'http://') <> 0 then
            BaseIndex := 8;

        Baseurl := CopyStr(RedirectUrl, BaseIndex);
        EndBaseUrlIndex := StrPos(Baseurl, '/');

        if EndBaseUrlIndex = 0 then
            exit(RedirectUrl);

        Baseurl := CopyStr(Baseurl, 1, EndBaseUrlIndex - 1);
        exit(CopyStr(RedirectUrl, 1, BaseIndex - 1) + Baseurl);
    end;

    procedure GetO365Resource(): Text
    var
        UrlHelper: Codeunit "Url Helper";
    begin
        exit(UrlHelper.GetO365Resource());
    end;

    procedure GetO365ResourceName(): Text
    begin
        exit(O365ResourceNameTxt);
    end;

    procedure IsSaaS(): Boolean
    var
        EnvironmentInfo: Codeunit "Environment Information";
    begin
        exit(EnvironmentInfo.IsSaaSInfrastructure());
    end;

    local procedure GetClientId() ClientID: Text
    begin
        if IsSaaS() then begin
            AzureADAuthFlow.Initialize(GetRedirectUrl());
            ClientID := AzureADAuthFlow.GetSaasClientId();
        end else begin
            if AzureADAppSetup.IsEmpty() then
                Error(AzureADNotSetupErr, ProductName.Short());

            AzureADAppSetup.FindFirst();
            ClientID := LowerCase(Format(AzureADAppSetup."App ID", 0, 4));
        end;
    end;

    [Scope('OnPrem')]
    procedure GetInitialTenantDomainName() InitialTenantDomainName: Text
    begin
        if IsSaaS() then begin
            AzureADAuthFlow.Initialize(GetRedirectUrl());
            InitialTenantDomainName := AzureADAuthFlow.GetInitialTenantDomainName();
        end;
    end;

    procedure IsAzureADAppSetupDone(): Boolean
    begin
        if (not IsSaaS()) and AzureADAppSetup.IsEmpty() then
            exit(false);

        exit(true);
    end;

    [Scope('OnPrem')]
    procedure CreateExchangeServiceWrapperWithToken(Token: SecretText; var Service: DotNet ExchangeServiceWrapper)
    begin
        AzureADAuthFlow.CreateExchangeServiceWrapperWithToken(Token, Service);
    end;

    [Scope('OnPrem')]
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Action Triggers", 'GetUserToken', '', false, false)]
    [NonDebuggable]
    local procedure OnGetUserToken(Resource: Text; Scenario: Text; var Token: Text)
    begin
        Token := GetAccessTokenAsSecretText(Resource, Resource, false).Unwrap();
    end;

    [TryFunction]
    [NonDebuggable]
    local procedure AcquireGuestToken(ResourceName: Text; GuestTenantId: Text; var AccessToken: Text)
    begin
        if IsSaaS() then begin
            // This is SaaS-only functionality at this point, so On-Prem/PaaS will not retrieve an access token
            AzureADAuthFlow.Initialize(GetRedirectUrl());
            AccessToken := AzureADAuthFlow.AcquireGuestToken(ResourceName, GuestTenantId);
        end else
            AccessToken := '';
    end;

    [TryFunction]
    local procedure AcquireToken(ResourceName: Text; var AccessToken: SecretText)
    begin
        // This function will return access token for a resource
        // Need to run the Azure AD Setup wizard before calling into this.
        // Returns empty string if access token not available

        AzureADAuthFlow.Initialize(GetRedirectUrl());

        if IsSaaS() then
            AccessToken := AzureADAuthFlow.AcquireTokenFromCacheAsSecretText(ResourceName)
        else begin
            AzureADAppSetup.FindFirst();
            AccessToken := AzureADAuthFlow.AcquireTokenFromCacheWithCredentialsAsSecretText(
                GetClientId(),
                AzureADAppSetup.GetSecretKeyFromIsolatedStorageAsSecretText(),
                ResourceName);
        end;
    end;

    [NonDebuggable]
    [Scope('OnPrem')]
    procedure GetLastErrorMessage(): Text
    var
        AuthenticationError: Text;
    begin
        AuthenticationError := AzureADAuthFlow.GetLastErrorMessage();
        if AuthenticationError <> '' then
            exit(AuthenticationError);

        exit(GetLastErrorText());
    end;
}