Skip to content

Implement Device Code Flow Authentication and Bulk Login - #1

Draft
google-labs-jules[bot] wants to merge 6 commits into
mainfrom
device-code-flow-bulk-login-15901970604348367866
Draft

Implement Device Code Flow Authentication and Bulk Login#1
google-labs-jules[bot] wants to merge 6 commits into
mainfrom
device-code-flow-bulk-login-15901970604348367866

Conversation

@google-labs-jules

Copy link
Copy Markdown

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

- 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.
@google-labs-jules

Copy link
Copy Markdown
Author

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.');

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Suggested change
Error('Control not ready.');
Error('The login control is still initializing. Please wait a moment and try again.');

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +31
}, function(err) {
console.error('Copy failed', err);
alert('Failed to copy code. Please manually copy: ' + currentLoginData.userCode);
});

// Open window
window.open(currentLoginData.verificationUrl, '_blank');

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}, 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
);
});

Copilot uses AI. Check for mistakes.
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);

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +67
pollIntervals[tenantId] = setInterval(function() {
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('PollToken', [tenantId, deviceCode, clientId]);
}, 5000);

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,154 @@
namespace D4P.CCMS.Auth;

using D4P.CCMS.Auth;

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The namespace 'D4P.CCMS.Auth' is imported but this file is already in the same namespace (line 1). This is a redundant import.

Suggested change
using D4P.CCMS.Auth;

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +29
begin
TenantIdText := Format(TenantId);
TenantIdText := DelChr(TenantIdText, '=', '{}');
Url := StrSubstNo(DeviceCodeUrlLbl, TenantIdText);

Content.WriteFrom(StrSubstNo('client_id=%1&scope=%2', Format(ClientId), ScopeLbl));

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +42
if IsNullGuid(ClientId) then
ClientId := Tenant."Client ID";

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +50
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);

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +124 to +129
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;

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +67
function StartPollingInternal(tenantId, deviceCode, clientId) {
if (pollIntervals[tenantId]) {
clearInterval(pollIntervals[tenantId]);
}

pollIntervals[tenantId] = setInterval(function() {
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('PollToken', [tenantId, deviceCode, clientId]);
}, 5000);

Copilot AI Jan 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the detailed feedback. I have updated the implementation to respect the interval returned by the Device Code endpoint.

  1. RequestDeviceCode now extracts the interval (defaulting to 5 if missing).
  2. This interval is passed to the Page and then to the JavaScript control.
  3. 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.
@adamLiveware

Copy link
Copy Markdown
Owner

@jules can you see Copilot's review and recommendations?

@google-labs-jules

Copy link
Copy Markdown
Author

@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.

Copilot AI commented Jan 6, 2026

Copy link
Copy Markdown

@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.

Copilot AI commented Jan 6, 2026

Copy link
Copy Markdown

@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.

google-labs-jules Bot and others added 2 commits January 6, 2026 10:05
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants