Skip to content

Commit

Permalink
SharePoint Auth - Improved README and use the Token Cache (microsoft#…
Browse files Browse the repository at this point in the history
…21591)

After some fighting to get the SharePoint module to work, I want to
improve the SharePoint Authorization module with my findings.

1. Improved the README.md file
- Added information and examples of API Permissions and Scope needed to
get the authorization to work.
    - Corrected the syntaxes of the functions
2. Added support for Token Cache when acquiring an Access Token
- This removes unnecessary popups of the authorization window, and I
believe it is faster
- This makes it possible to schedule jobs that works against SharePoint,
as long as the user is authenticated now and then (I believe the default
life for a Refresh Token is 3 months)
3. Removed the custom caching of Access Token.
- "SharePoint Authorization Code" had implemented it's own caching of
the Access Token, with a hardcoded lifetime of 1 hour. From what I read,
that would probably work for most cases since the lifetime of an Access
Token is between 60 and 90 minutes. But as stated
[here](https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-configurable-token-lifetimes),
the lifetime of an Access Token can be configured to a shorter time,
that would not work together with this "custom" token cache. A custom
caching must read the Access Token lifetime to be water proof, but the
easy fix here is to just remove it. :)
    - This change leaves the caching to the OAuth2 module.
4. Reworked the logic around the `IsSuccess` return variable.
- This had some issues, where `AcquireTokenByAuthorizationCode()` would
return true (it is a TryFunction, and some issues with acquiring an
access token does not result in an `Error()`), but with an empty Access
Token. This made me spend waaay to much time troubleshooting this
codeunit... The only thing that really tells us if acquiring an Access
Token failed or succeeded is if the Access Token has a value or not.

The tests are unaffected by this change.
  • Loading branch information
jwikman authored Jan 9, 2023
1 parent dc6f37c commit f816960
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 24 deletions.
89 changes: 83 additions & 6 deletions Modules/System/SharePoint Authorization/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,95 @@
This module provides functionality for authenticating to the SharePoint REST API.
This module does not permanently store any data.

### Create Client Credentials
Creates Codeunit implementing "SharePoint Authorization" interface that can later be used with SharePoint module.
# Public Objects

## "SharePoint Auth."

### Example

This example shows how to use the `CreateAuthorizationCode()` function when saving a file to a SharePoint Online library.

```
[NonDebuggable]
procedure CreateUserCredentials(AadTenantId: Text; ClientId: Text; UserName: Text; Credential: Text; Scopes: List of [Text]): Interface "SharePoint Authorization";
internal procedure SaveFile(BaseUrl: Text; LibraryAndFolderPath: Text; Filename: Text; var TempBlob: Codeunit "Temp Blob")
var
SharePointFile: Record "SharePoint File";
SharePointClient: Codeunit "SharePoint Client";
SaveFailedErr: Label 'Save to SharePoint failed.\ErrorMessage: %1\HttpRetryAfter: %2\HttpStatusCode: %3\ResponseReasonPhrase: %4', Comment = '%1=GetErrorMessage; %2=GetHttpRetryAfter; %3=GetHttpStatusCode; %4=GetResponseReasonPhrase';
AadTenantId: Text;
IS: InStream;
Diag: Interface "HTTP Diagnostics";
begin
AadTenantId := GetAadTenantNameFromBaseUrl(BaseUrl);
SharePointClient.Initialize(BaseUrl, GetSharePointAuthorization(AadTenantId));
IS := TempBlob.CreateInStream();
if not SharePointClient.AddFileToFolder(LibraryAndFolderPath, Filename, IS, SharePointFile) then begin
Diag := SharePointClient.GetDiagnostics();
Error(SaveFailedErr, Diag.GetErrorMessage(), Diag.GetHttpRetryAfter(), Diag.GetHttpStatusCode(), Diag.GetResponseReasonPhrase());
end;
end;
local procedure GetSharePointAuthorization(AadTenantId: Text): Interface "SharePoint Authorization"
var
SharePointAuth: Codeunit "SharePoint Auth.";
Scopes: List of [Text];
ClientId: Text;
[NonDebuggable]
ClientSecret: Text;
begin
GetAppRegistration(ClientId, ClientSecret);
Scopes.Add('00000003-0000-0ff1-ce00-000000000000/.default');
exit(SharePointAuth.CreateAuthorizationCode(AadTenantId, ClientId, ClientSecret, Scopes));
end;
local procedure GetAadTenantNameFromBaseUrl(BaseUrl: Text): Text
var
Uri: Codeunit Uri;
MySiteHostSuffixTxt: Label '-my.sharepoint.com', Locked = true;
SharePointHostSuffixTxt: Label '.sharepoint.com', Locked = true;
OnMicrosoftTxt: Label '.onmicrosoft.com', Locked = true;
UrlInvalidErr: Label 'The Base Url %1 does not seem to be a valid SharePoint Online Url.', Comment = '%1=BaseUrl';
Host: Text;
begin
// SharePoint Online format: https://tenantname.sharepoint.com/SiteName/LibraryName/
// SharePoint My Site format: https://tenantname-my.sharepoint.com/personal/user_name/
Uri.Init(BaseUrl);
Host := Uri.GetHost();
if not Host.EndsWith(SharePointHostSuffixTxt) then
Error(UrlInvalidErr, BaseUrl);
if Host.EndsWith(MySiteHostSuffixTxt) then
exit(CopyStr(Host, 1, StrPos(Host, MySiteHostSuffixTxt) - 1) + OnMicrosoftTxt);
exit(CopyStr(Host, 1, StrPos(Host, SharePointHostSuffixTxt) - 1) + OnMicrosoftTxt);
end;
```

### Create Client Credentials

Creates a Codeunit implementing "SharePoint Authorization" interface that can later be used with SharePoint module.

This implementation is using the OAuth 2.0 Authorization Code Flow, which means that the access to SharePoint will be performed with the credentials of the currently logged on user, i.e. the user permissions will apply.

The App Registration specified by the ClientId parameter should be assigned the _Delegated_ API Permissions that is needed for the intended operations in the "SharePoint Client" codeunit.

Examples of delegated API Permissions on the **SharePoint** resource:
| Resource | API Permission | Used for |
| -------- | -------------- | -------- |
| SharePoint | AllSites.Write | Reading and writing files on a regular SharePoint site |
| SharePoint | MyFiles.Write | Reading and writing files on the SharePoint My Site (The Personal OneDrive site) |

### CreateAuthorizationCode

Creates an authorization mechanism with authentication code.

'00000003-0000-0ff1-ce00-000000000000/.default' can be used as the `Scope` parameter, where the guid is the Application Id for Office 365 SharePoint Online.

#### Syntax

```
[NonDebuggable]
procedure CreateUserCredentials(AadTenantId: Text; ClientId: Text; UserName: Text; Credential: Text; Scope: Text): Interface "SharePoint Authorization";
procedure CreateAuthorizationCode(AadTenantId: Text; ClientId: Text; ClientSecret: Text; Scopes: List of [Text]): Interface "SharePoint Authorization";
```


```
[NonDebuggable]
procedure CreateAuthorizationCode(AadTenantId: Text; ClientId: Text; ClientSecret: Text; Scope: Text): Interface "SharePoint Authorization";
```
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@ codeunit 9144 "SharePoint Authorization Code" implements "SharePoint Authorizati
[NonDebuggable]
ClientSecret: Text;
[NonDebuggable]
AccessToken: Text;
[NonDebuggable]
AuthCodeErr: Text;
[NonDebuggable]
AadTenantId: Text;
[NonDebuggable]
Scopes: List of [Text];
[NonDebuggable]
ExpiryDate: DateTime;
AuthorityTxt: Label 'https://login.microsoftonline.com/%1/oauth2/v2.0/authorize', Comment = '%1 = AAD tenant ID', Locked = true;
BearerTxt: Label 'Bearer %1', Comment = '%1 - Token', Locked = true;

Expand All @@ -32,8 +28,6 @@ codeunit 9144 "SharePoint Authorization Code" implements "SharePoint Authorizati
ClientId := NewClientId;
ClientSecret := NewClientSecret;
Scopes := NewScopes;
AccessToken := '';
ExpiryDate := 0DT;
end;

[NonDebuggable]
Expand All @@ -49,31 +43,33 @@ codeunit 9144 "SharePoint Authorization Code" implements "SharePoint Authorizati
local procedure GetToken(): Text
var
ErrorText: Text;
[NonDebuggable]
AccessToken: Text;
begin
if (AccessToken = '') or (AccessToken <> '') and (ExpiryDate > CurrentDateTime()) then
if not AcquireToken(ErrorText) then
Error(ErrorText)
else
ExpiryDate := CurrentDateTime() + (3599 * 1000);
if not AcquireToken(AccessToken, ErrorText) then
Error(ErrorText);
exit(AccessToken);
end;

[NonDebuggable]
local procedure AcquireToken(var ErrorText: Text): Boolean
local procedure AcquireToken(var AccessToken: Text; var ErrorText: Text): Boolean
var
OAuth2: Codeunit OAuth2;
IsHandled, IsSuccess : Boolean;
begin
OnBeforeGetToken(IsHandled, IsSuccess, ErrorText, AccessToken);

if not IsHandled then begin
IsSuccess := OAuth2.AcquireTokenByAuthorizationCode(ClientId, ClientSecret, StrSubstNo(AuthorityTxt, AadTenantId), '', Scopes, "Prompt Interaction"::None, AccessToken, AuthCodeErr);
if (not OAuth2.AcquireAuthorizationCodeTokenFromCache(ClientId, ClientSecret, '', StrSubstNo(AuthorityTxt, AadTenantId), Scopes, AccessToken)) or (AccessToken = '') then
OAuth2.AcquireTokenByAuthorizationCode(ClientId, ClientSecret, StrSubstNo(AuthorityTxt, AadTenantId), '', Scopes, "Prompt Interaction"::None, AccessToken, AuthCodeErr);

IsSuccess := AccessToken <> '';

if AuthCodeErr <> '' then
ErrorText := AuthCodeErr
else
ErrorText := GetLastErrorText();

if not IsSuccess then
if AuthCodeErr <> '' then
ErrorText := AuthCodeErr
else
ErrorText := GetLastErrorText();
end;

exit(IsSuccess);
Expand Down

0 comments on commit f816960

Please sign in to comment.