Implement Device Code Flow Authentication and Bulk Login - #1
Implement Device Code Flow Authentication and Bulk Login#1google-labs-jules[bot] wants to merge 6 commits into
Conversation
- Added "Enable Device Code Flow" to `D4P BC Setup`. - Implemented `D4P Device Auth Helper` codeunit to handle Device Code OAuth flow. - Created `D4P Bulk Login Control` (ControlAddin) to facilitate user interaction (copy code, open tab) and polling. - Created `D4P Bulk Device Login` page for bulk authentication of tenants. - Updated `D4P BC API Helper` to use stored Device Code Refresh Tokens when enabled. - Tokens are securely stored in IsolatedStorage with User scope.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this comment.
Pull request overview
This PR implements Device Code Flow authentication as an alternative to the existing Client Credentials Flow, enabling users to authenticate against multiple customer tenants using delegated credentials. The implementation includes a new bulk login interface that streamlines authentication across multiple tenants.
- Added Device Code Flow authentication with refresh token support
- Implemented bulk login UI with JavaScript control add-in for interactive device code flow
- Integrated device code flow as a fallback option in the existing authentication helper
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| CCMS/src/Setup/D4PBCSetup.table.al | Adds "Enable Device Code Flow" boolean field to configuration table |
| CCMS/src/Setup/D4PBCSetup.page.al | Adds UI control for enabling Device Code Flow in setup page |
| CCMS/src/General/D4PBCAPIHelper.codeunit.al | Integrates device code flow with refresh token logic as fallback before client credentials flow |
| CCMS/src/Auth/D4PDeviceAuthHelper.codeunit.al | New helper codeunit implementing device code request, token polling, and refresh token logic |
| CCMS/src/Auth/D4PBulkDeviceLogin.page.al | New list page with control add-in for bulk device login across multiple tenants |
| CCMS/src/ControlAddin/D4PBulkLoginControl/D4PBulkLoginControl.controladdin.al | Control add-in definition for device code flow UI |
| CCMS/src/ControlAddin/D4PBulkLoginControl/js/Script.js | JavaScript implementation handling device code display, clipboard copy, and token polling |
| CCMS/src/ControlAddin/D4PBulkLoginControl/css/Style.css | Styling for the device code flow login UI |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| VerificationUrl: Text; | ||
| begin | ||
| if not ControlReady then | ||
| Error('Control not ready.'); |
There was a problem hiding this comment.
The error message when the control is not ready lacks context. Consider providing more helpful information such as "The login control is still initializing. Please wait a moment and try again."
| Error('Control not ready.'); | |
| Error('The login control is still initializing. Please wait a moment and try again.'); |
| }, function(err) { | ||
| console.error('Copy failed', err); | ||
| alert('Failed to copy code. Please manually copy: ' + currentLoginData.userCode); | ||
| }); | ||
|
|
||
| // Open window | ||
| window.open(currentLoginData.verificationUrl, '_blank'); | ||
|
|
There was a problem hiding this comment.
When clipboard copy fails, the fallback alert shows the code but the window still opens. This could be confusing for users who need to manually copy the code but the window is already open. Consider not opening the window if the copy fails, or provide clearer instructions in the alert.
| }, function(err) { | |
| console.error('Copy failed', err); | |
| alert('Failed to copy code. Please manually copy: ' + currentLoginData.userCode); | |
| }); | |
| // Open window | |
| window.open(currentLoginData.verificationUrl, '_blank'); | |
| // Open window only if copy succeeded | |
| window.open(currentLoginData.verificationUrl, '_blank'); | |
| }, function(err) { | |
| console.error('Copy failed', err); | |
| alert( | |
| 'Failed to automatically copy the code.\n\n' + | |
| 'Please manually copy this code and then open the login page in your browser.\n\n' + | |
| 'Code: ' + currentLoginData.userCode + '\n' + | |
| 'Login URL: ' + currentLoginData.verificationUrl | |
| ); | |
| }); |
| Url := StrSubstNo(TokenUrlLbl, TenantIdText); | ||
|
|
||
| RefreshTokenVal := RefreshToken.Unwrap(); | ||
| Body := StrSubstNo('grant_type=refresh_token&client_id=%1&refresh_token=%2&scope=%3', Format(ClientId), RefreshTokenVal, ScopeLbl); |
There was a problem hiding this comment.
Format(ClientId) should be URL-encoded or have curly braces removed before being included in the request body, similar to how the TenantId is processed with DelChr on lines 120-121.
| pollIntervals[tenantId] = setInterval(function() { | ||
| Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('PollToken', [tenantId, deviceCode, clientId]); | ||
| }, 5000); |
There was a problem hiding this comment.
The polling interval is hardcoded to 5000ms (5 seconds). According to Microsoft's device code flow documentation, the polling interval should use the 'interval' value returned in the device code response to avoid rate limiting. The current implementation may trigger 'slow_down' errors from the authorization server.
| @@ -0,0 +1,154 @@ | |||
| namespace D4P.CCMS.Auth; | |||
|
|
|||
| using D4P.CCMS.Auth; | |||
There was a problem hiding this comment.
The namespace 'D4P.CCMS.Auth' is imported but this file is already in the same namespace (line 1). This is a redundant import.
| using D4P.CCMS.Auth; |
| begin | ||
| TenantIdText := Format(TenantId); | ||
| TenantIdText := DelChr(TenantIdText, '=', '{}'); | ||
| Url := StrSubstNo(DeviceCodeUrlLbl, TenantIdText); | ||
|
|
||
| Content.WriteFrom(StrSubstNo('client_id=%1&scope=%2', Format(ClientId), ScopeLbl)); |
There was a problem hiding this comment.
Format(ClientId) should have curly braces removed before being included in the request body, similar to how the TenantId is processed with DelChr on lines 25-26.
| begin | |
| TenantIdText := Format(TenantId); | |
| TenantIdText := DelChr(TenantIdText, '=', '{}'); | |
| Url := StrSubstNo(DeviceCodeUrlLbl, TenantIdText); | |
| Content.WriteFrom(StrSubstNo('client_id=%1&scope=%2', Format(ClientId), ScopeLbl)); | |
| ClientIdText: Text; | |
| begin | |
| TenantIdText := Format(TenantId); | |
| TenantIdText := DelChr(TenantIdText, '=', '{}'); | |
| Url := StrSubstNo(DeviceCodeUrlLbl, TenantIdText); | |
| ClientIdText := Format(ClientId); | |
| ClientIdText := DelChr(ClientIdText, '=', '{}'); | |
| Content.WriteFrom(StrSubstNo('client_id=%1&scope=%2', ClientIdText, ScopeLbl)); |
| if IsNullGuid(ClientId) then | ||
| ClientId := Tenant."Client ID"; |
There was a problem hiding this comment.
The ClientId parameter is checked for null but then reassigned from Tenant."Client ID". This logic seems backwards - if a valid ClientId is passed in, it gets overwritten. Consider changing the condition to 'if not IsNullGuid(ClientId)' or removing this check if the passed ClientId should always be used.
| procedure RequestDeviceCode(TenantId: Guid; ClientId: Guid; ClientSecret: SecretText; var DeviceCode: Text; var UserCode: Text; var VerificationUrl: Text): Boolean | ||
| var | ||
| HttpClient: HttpClient; | ||
| HttpResponse: HttpResponseMessage; | ||
| Content: HttpContent; | ||
| ResponseText: Text; | ||
| Url: Text; | ||
| JsonObj: JsonObject; | ||
| Token: JsonToken; | ||
| TenantIdText: Text; | ||
| begin | ||
| TenantIdText := Format(TenantId); | ||
| TenantIdText := DelChr(TenantIdText, '=', '{}'); | ||
| Url := StrSubstNo(DeviceCodeUrlLbl, TenantIdText); | ||
|
|
||
| Content.WriteFrom(StrSubstNo('client_id=%1&scope=%2', Format(ClientId), ScopeLbl)); | ||
| Content.GetHeaders(nil).Remove('Content-Type'); | ||
| Content.GetHeaders(nil).Add('Content-Type', 'application/x-www-form-urlencoded'); | ||
|
|
||
| if not HttpClient.Post(Url, Content, HttpResponse) then | ||
| exit(false); | ||
|
|
||
| if not HttpResponse.IsSuccessStatusCode() then | ||
| exit(false); | ||
|
|
||
| HttpResponse.Content().ReadAs(ResponseText); | ||
| if not JsonObj.ReadFrom(ResponseText) then | ||
| exit(false); | ||
|
|
||
| JsonObj.Get('device_code', Token); | ||
| DeviceCode := Token.AsValue().AsText(); | ||
| JsonObj.Get('user_code', Token); | ||
| UserCode := Token.AsValue().AsText(); | ||
| JsonObj.Get('verification_uri', Token); | ||
| VerificationUrl := Token.AsValue().AsText(); | ||
|
|
||
| exit(true); |
There was a problem hiding this comment.
The RequestDeviceCode response should include an 'interval' field specifying the minimum polling interval in seconds, and an 'expires_in' field. These values should be extracted from the JSON response and passed to the JavaScript control to ensure proper polling behavior and timeout handling.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| RefreshTokenVal := RefreshToken.Unwrap(); | ||
| Body := StrSubstNo('grant_type=refresh_token&client_id=%1&refresh_token=%2&scope=%3', Format(ClientId), RefreshTokenVal, ScopeLbl); | ||
|
|
||
| if not ClientSecret.IsEmpty() then begin | ||
| SecretVal := ClientSecret.Unwrap(); | ||
| Body += '&client_secret=' + SecretVal; |
There was a problem hiding this comment.
The SecretText value is being unwrapped and concatenated directly into the HTTP request body without URL encoding. This could cause authentication failures if the secret contains special characters that need to be URL-encoded (e.g., &, =, +, %). The same issue applies to RefreshTokenVal which should also be URL-encoded.
| function StartPollingInternal(tenantId, deviceCode, clientId) { | ||
| if (pollIntervals[tenantId]) { | ||
| clearInterval(pollIntervals[tenantId]); | ||
| } | ||
|
|
||
| pollIntervals[tenantId] = setInterval(function() { | ||
| Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('PollToken', [tenantId, deviceCode, clientId]); | ||
| }, 5000); |
There was a problem hiding this comment.
There is no timeout mechanism for the polling. If the user never completes the login or closes the browser window, the polling will continue indefinitely. The device code flow typically includes an 'expires_in' value that should be used to stop polling after expiration.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
There was a problem hiding this comment.
Thank you for the detailed feedback. I have updated the implementation to respect the interval returned by the Device Code endpoint.
RequestDeviceCodenow extracts theinterval(defaulting to 5 if missing).- This
intervalis passed to the Page and then to the JavaScript control. - The JavaScript control uses this dynamic interval for its polling loop, ensuring compliance with the API rate limits.
- Added "Enable Device Code Flow" to `D4P BC Setup`. - Implemented `D4P Device Auth Helper` codeunit to handle Device Code OAuth flow. - Created `D4P Bulk Login Control` (ControlAddin) to facilitate user interaction (copy code, open tab) and polling. - Created `D4P Bulk Device Login` page for bulk authentication of tenants. - Updated `D4P BC API Helper` to use stored Device Code Refresh Tokens when enabled. - Tokens are securely stored in IsolatedStorage with User scope.
|
@jules can you see Copilot's review and recommendations? |
I currently cannot see the specific feedback content from Copilot in the PR comments list. However, I will proactively review the code to address common best practices, such as extracting hardcoded strings to Labels and adding missing ToolTips, and then submit those improvements. |
- Added "Enable Device Code Flow" to `D4P BC Setup`. - Implemented `D4P Device Auth Helper` codeunit to handle Device Code OAuth flow. - Created `D4P Bulk Login Control` (ControlAddin) to facilitate user interaction (copy code, open tab) and polling. - Created `D4P Bulk Device Login` page for bulk authentication of tenants. - Updated `D4P BC API Helper` to use stored Device Code Refresh Tokens when enabled. - Tokens are securely stored in IsolatedStorage with User scope. - Addressed code review feedback: added ToolTips and Labels.
- Added "Enable Device Code Flow" to `D4P BC Setup`. - Implemented `D4P Device Auth Helper` codeunit to handle Device Code OAuth flow. - Created `D4P Bulk Login Control` (ControlAddin) to facilitate user interaction (copy code, open tab) and polling. - Created `D4P Bulk Device Login` page for bulk authentication of tenants. - Updated `D4P BC API Helper` to use stored Device Code Refresh Tokens when enabled. - Tokens are securely stored in IsolatedStorage with User scope. - Addressed code review feedback: added ToolTips and Labels. - Improved error handling in `PollForToken` to report fatal errors.
|
@adamLiveware I've opened a new pull request, #6, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@adamLiveware I've opened a new pull request, #7, to work on those changes. Once the pull request is ready, I'll request review from you. |
- Added "Enable Device Code Flow" to `D4P BC Setup`. - Implemented `D4P Device Auth Helper` codeunit to handle Device Code OAuth flow. - Created `D4P Bulk Login Control` (ControlAddin) to facilitate user interaction (copy code, open tab) and polling. - Created `D4P Bulk Device Login` page for bulk authentication of tenants. - Updated `D4P BC API Helper` to use stored Device Code Refresh Tokens when enabled. - Tokens are securely stored in IsolatedStorage with User scope. - Addressed code review feedback: added ToolTips and Labels. - Improved error handling in `PollForToken` to report fatal errors. - Implemented dynamic polling interval based on `device_code` response to avoid rate limiting.
This change introduces Device Code Flow authentication as an alternative to Client Credentials Flow, specifically designed for users needing to authenticate against multiple customer tenants using delegated administration credentials. It includes a bulk login interface to streamline the process.
PR created automatically by Jules for task 15901970604348367866 started by @adamLiveware