Skip to content

Conversation

@jpmchia
Copy link

@jpmchia jpmchia commented Jan 21, 2026

Describe your changes

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • Logto identity provider integration now available, enabling user authentication and management through Logto's identity services with support for user creation, retrieval, and account management operations.
  • Chores

    • Updated configuration templates and environment setup documentation to include Logto-specific parameters, authentication endpoints, and tenant configuration guidance for both Cloud and OSS deployments.

✏️ Tip: You can customize this high-level summary in your review settings.

braginini and others added 7 commits January 10, 2026 22:07
- Introduced NETBIRD_MGMT_EXTRA_AUTH_AUDIENCE in base.setup.env for additional audience configuration.
- Updated management.json.tmpl to include ExtraAuthAudience for LogTo integration.
- Enhanced setup.env.exampale with LogTo-specific OIDC configuration and redirect URI instructions.
- Modified idp.go to handle LogTo client credentials and resource URL construction for management API.
Merged 26 commits from main including:
- Local user password change (embedded IdP) (netbirdio#5132)
- [Management/Client] Trigger debug bundle runs from API/Dashboard
- [client] Add support to wildcard custom records
- Feature/embedded STUN
- [management] Add custom dns zones
- Various bug fixes and improvements

Resolved conflict in idp/dex/provider.go by:
- Keeping UpdateUserPassword method from main
- Preserving connector management code from idp-logto (in connector.go)
Copilot AI review requested due to automatic review settings January 21, 2026 03:01
@CLAassistant
Copy link

CLAassistant commented Jan 21, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ braginini
✅ jpmchia
jpmchia@terra-net.uk


jpmchia@terra-net.uk seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

The pull request introduces Logto as a new identity provider for the system. Changes include configuration variables, template updates, documentation enhancements, and a complete Logto IdP manager implementation with user management operations and JWT token authentication.

Changes

Cohort / File(s) Summary
Configuration Variables
infrastructure_files/base.setup.env
Added NETBIRD_MGMT_EXTRA_AUTH_AUDIENCE environment variable with empty default and export directive.
Configuration Template
infrastructure_files/management.json.tmpl
Added ExtraAuthAudience field under HttpConfig.OIDCConfig and LogtoClientCredentials field under IdpManagerConfig.ExtraConfig.
Documentation & Examples
infrastructure_files/setup.env.example
Expanded OIDC configuration with LogTo-specific endpoints, comments for Cloud/OSS variants, device authorization flow provider guidance, IDP management endpoints, and clarified redirect URI handling with path-based examples.
IdP Manager Integration
management/server/idp/idp.go
Extended IdP manager switch statement to instantiate LogtoManager when configured; added LogtoClientCredentials field to Config struct for Logto configuration.
Logto Manager Implementation
management/server/idp/logto.go
New file introducing LogtoManager with complete IdP integration: JWT token authentication and caching, user CRUD operations (CreateUser, GetUserByEmail, GetUserDataByID, DeleteUser), account retrieval (GetAccount, GetAllAccounts), HTTP helpers with automatic JWT injection, and internal data structures and mapping utilities.
Logto Manager Tests
management/server/idp/logto_test.go
New file with comprehensive unit test coverage for LogtoManager configuration validation, JWT token request/parsing/validity flow, authentication, and profile handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Management Server
    participant TokenMgr as LogtoCredentials
    participant HTTP as HTTP Client
    participant LogtoAPI as Logto API
    
    Client->>TokenMgr: Authenticate()
    TokenMgr->>TokenMgr: Check if JWT still valid
    alt JWT expired or missing
        TokenMgr->>HTTP: POST /oidc/token
        HTTP->>LogtoAPI: Request JWT Token
        LogtoAPI-->>HTTP: Return access_token, expires_in
        HTTP-->>TokenMgr: Parse response
        TokenMgr->>TokenMgr: Store token & expiry time
    else JWT valid
        TokenMgr-->>TokenMgr: Reuse cached token
    end
    TokenMgr-->>Client: Return JWTToken

    Client->>HTTP: GET /api/users (with JWT header)
    HTTP->>LogtoAPI: Fetch user data
    LogtoAPI-->>HTTP: Return logtoProfile[]
    HTTP->>Client: Map to UserData[]
Loading
sequenceDiagram
    participant App as Management Server
    participant Manager as LogtoManager
    participant Creds as LogtoCredentials
    participant LogtoAPI as Logto API
    
    App->>Manager: CreateUser(email, name, accountID, invitedByEmail)
    Manager->>Creds: Authenticate()
    Creds->>LogtoAPI: Request JWT Token
    LogtoAPI-->>Creds: Return access_token
    Manager->>LogtoAPI: POST /api/users with JWT
    LogtoAPI-->>Manager: Return created user profile
    Manager->>Manager: Map logtoProfile to UserData
    Manager-->>App: Return UserData
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • mlsmaycon
  • bcmmbaga

Poem

🐰 A new IdP joins the warren today,
Logto hops in, ready to play!
With tokens and users all secure and tight,
Authentication flows through the night. 🌙
Code review this patch, we'll get it just right!

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is completely unfilled—only the template structure remains with no actual content describing changes, issue links, or decisions. Complete all required sections: describe the changes made, link the issue ticket, clarify the type of change (feature/bug/refactor), confirm test coverage, and address documentation needs.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Idp logto" is vague and lacks clarity; it doesn't convey what changes are being made or their purpose beyond mentioning logto. Use a more descriptive title such as "Add LogTo identity provider support" or "Implement LogTo IdP integration" to clearly communicate the main change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jpmchia jpmchia marked this pull request as draft January 21, 2026 03:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds support for Logto as an identity provider (IDP) option in the NetBird management server. Logto is an open-source authentication and identity solution that can be used in both cloud and self-hosted (OSS) configurations. The implementation follows the existing patterns used by other IDP integrations like Keycloak and Zitadel.

Changes:

  • Added complete Logto IDP integration with authentication, user management, and profile retrieval capabilities
  • Included comprehensive test coverage for core authentication and JWT token management functionality
  • Updated configuration templates and examples with Logto-specific documentation and setup instructions

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
management/server/idp/logto.go Core implementation of Logto IDP manager with authentication, user CRUD operations, and profile management
management/server/idp/logto_test.go Test suite covering manager initialization, JWT token handling, authentication flows, and profile data mapping
management/server/idp/idp.go Integration of Logto into the IDP factory with configuration handling and resource URL construction
infrastructure_files/setup.env.example Documentation and examples for Logto-specific configuration including OIDC endpoints, API resources, and redirect URIs
infrastructure_files/management.json.tmpl Template updates to support Logto credentials and ExtraAuthAudience configuration
infrastructure_files/base.setup.env Environment variable definitions for ExtraAuthAudience support

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

creds := LogtoCredentials{
clientConfig: config,
helper: testCase.helper,
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Inconsistent indentation. This closing brace is indented with tabs instead of being aligned with the opening of the struct definition above. It should be properly aligned at the same level as the creds := LogtoCredentials{ line.

Suggested change
}
}

Copilot uses AI. Check for mistakes.
return nil, err
}

var pending bool = true
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Redundant variable declaration. The variable pending is explicitly declared as var pending bool = true. In Go, this can be simplified to pending := true which is more idiomatic and concise.

Suggested change
var pending bool = true
pending := true

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +175
if resp.StatusCode != http.StatusOK {
if lc.appMetrics != nil {
lc.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to get logto token, statusCode %d", resp.StatusCode)
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Missing metrics tracking for status code errors in requestJWTToken. Unlike the Authenticate method and other request methods (get, post, DeleteUser), this function doesn't call lc.appMetrics.IDPMetrics().CountRequestStatusError() before returning an error when the status code is not 200. This creates inconsistency in metrics tracking.

Copilot uses AI. Check for mistakes.
creds := LogtoCredentials{
clientConfig: config,
jwtToken: testCase.inputToken,
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Inconsistent indentation. This closing brace is indented with tabs instead of being aligned with the opening of the struct definition above. It should be properly aligned at the same level as the creds := LogtoCredentials{ line.

Suggested change
}
}

Copilot uses AI. Check for mistakes.
jpmchia and others added 3 commits January 21, 2026 03:10
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@management/server/idp/idp.go`:
- Around line 212-240: The resource default logic in the "logto" case builds a
cloud URL regardless of deployment; change it so when
config.ExtraConfig["Resource"] is empty you derive the resource from the
management endpoint for self-hosted deployments (or require explicit Resource):
inspect config.ExtraConfig["ManagementEndpoint"] and if present use its origin
(scheme+host[:port]) plus the API path (e.g. "/api") as the resource, otherwise
fall back to the tenant-based cloud URL; update the block that constructs
logtoClientConfig (LogtoClientConfig.Resource and TenantID) and ensure
NewLogtoManager receives the corrected LogtoClientConfig so token validation
matches the management endpoint.

In `@management/server/idp/logto.go`:
- Around line 180-210: In parseRequestJWTResponse, guard against malformed JWTs
before indexing into the dot-split payload: after obtaining jwtToken.AccessToken
call strings.Split(jwtToken.AccessToken, ".") and verify the resulting slice has
at least 2 (preferably 3) parts; if not, return a descriptive error instead of
indexing [1]; keep existing base64.RawURLEncoding.DecodeString and Unmarshal
logic but only run them after this length check so JWTToken and IssuedAt
handling won’t panic on invalid tokens.
- Around line 138-177: In requestJWTToken, the http response body is leaked on
non-200 paths; after lc.httpClient.Do(req) succeeds and before returning an
error when resp.StatusCode != http.StatusOK, ensure you close (and ideally
drain) resp.Body (e.g., io.Copy(io.Discard, resp.Body) then resp.Body.Close())
so the HTTP transport connection can be reused; leave the body open only when
returning the successful resp to the caller.

Comment on lines +212 to +240
case "logto":
logtoClientConfig := config.LogtoClientCredentials
if config.ClientConfig != nil {
// Build resource URL from tenant ID or use provided resource
resource := config.ExtraConfig["Resource"]
if resource == "" {
tenantID := config.ExtraConfig["TenantID"]
if tenantID == "" {
tenantID = "default" // Default for OSS
}
resource = fmt.Sprintf("https://%s.logto.app/api", tenantID)
}

logtoClientConfig = &LogtoClientConfig{
ClientID: config.ClientConfig.ClientID,
ClientSecret: config.ClientConfig.ClientSecret,
GrantType: config.ClientConfig.GrantType,
TokenEndpoint: config.ClientConfig.TokenEndpoint,
ManagementEndpoint: config.ExtraConfig["ManagementEndpoint"],
Resource: resource,
TenantID: config.ExtraConfig["TenantID"],
}
}

if logtoClientConfig == nil {
return nil, fmt.Errorf("logto IdP configuration is missing")
}

return NewLogtoManager(*logtoClientConfig, appMetrics)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Logto client credentials flow resource audience configuration

💡 Result:

Short answer — use the OAuth2 token endpoint and pass the API identifier in the resource parameter (and scope as needed). For organization tokens also send organization_id.

How to:

  • POST to /oidc/token with Authorization: Basic base64(client_id:client_secret) and form body:
    • grant_type=client_credentials
    • resource=<your API identifier, e.g. https://api.example.com>
    • scope=<optional org/resource scopes or .default>
    • (for org-level tokens) organization_id=
  • The returned access_token will be issued for the resource/audience you specified; ensure your API validates that aud/resource matches this value. [1][2]

Sources:

  • Logto docs — Protect organization-level API resources / client credentials flow. [1]
  • Logto docs — Machine-to-machine (client credentials) overview. [2]

🏁 Script executed:

# Search for Logto-related configuration, tests, or documentation in the codebase
fd -type f -name "*.go" | xargs grep -l -i "logto" | head -20

Repository: netbirdio/netbird

Length of output: 231


🏁 Script executed:

# Check for any Logto configuration examples or tests
rg -i "logto.*resource|resource.*logto" -A 3 -B 3 --type go

Repository: netbirdio/netbird

Length of output: 4241


🏁 Script executed:

# Look for any comments or documentation about Logto setup
rg -i "logto.*app/api|tenant.*logto" -A 2 -B 2 --type go

Repository: netbirdio/netbird

Length of output: 1992


Fix Logto resource default for self-hosted deployments.
The code defaults resource to https://{tenant-id}.logto.app/api regardless of deployment type. However, tests show self-hosted configurations use local endpoints (https://localhost:3001/...), creating a mismatch: the resource URL will still target the cloud .logto.app domain while token and management endpoints point to localhost. For self-hosted Logto, the resource must match what the management API validates; use the management endpoint host instead of the hardcoded cloud format, or require explicit Resource configuration with validation.

🤖 Prompt for AI Agents
In `@management/server/idp/idp.go` around lines 212 - 240, The resource default
logic in the "logto" case builds a cloud URL regardless of deployment; change it
so when config.ExtraConfig["Resource"] is empty you derive the resource from the
management endpoint for self-hosted deployments (or require explicit Resource):
inspect config.ExtraConfig["ManagementEndpoint"] and if present use its origin
(scheme+host[:port]) plus the API path (e.g. "/api") as the resource, otherwise
fall back to the tenant-based cloud URL; update the block that constructs
logtoClientConfig (LogtoClientConfig.Resource and TenantID) and ensure
NewLogtoManager receives the corrected LogtoClientConfig so token validation
matches the management endpoint.

Comment on lines +138 to +177
// requestJWTToken performs request to get jwt token.
// LogTo uses Basic Authentication in header (not client_id/client_secret in body like Keycloak).
func (lc *LogtoCredentials) requestJWTToken(ctx context.Context) (*http.Response, error) {
// Basic auth: base64(appId:appSecret)
auth := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", lc.clientConfig.ClientID, lc.clientConfig.ClientSecret)),
)

data := url.Values{}
data.Set("grant_type", lc.clientConfig.GrantType)
data.Set("resource", lc.clientConfig.Resource) // Required parameter for LogTo
data.Set("scope", "all")

payload := strings.NewReader(data.Encode())
req, err := http.NewRequest(http.MethodPost, lc.clientConfig.TokenEndpoint, payload)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Basic "+auth) // Basic auth in header
req.Header.Add("content-type", "application/x-www-form-urlencoded")

log.WithContext(ctx).Debug("requesting new jwt token for logto idp manager")

resp, err := lc.httpClient.Do(req)
if err != nil {
if lc.appMetrics != nil {
lc.appMetrics.IDPMetrics().CountRequestError()
}

return nil, err
}

if resp.StatusCode != http.StatusOK {
if lc.appMetrics != nil {
lc.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to get logto token, statusCode %d", resp.StatusCode)
}

return resp, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Close the token response body on non‑200 paths.
Early returns on error status leak the response body, which can exhaust the HTTP transport pool over time.

🩹 Suggested fix
 	if resp.StatusCode != http.StatusOK {
 		if lc.appMetrics != nil {
 			lc.appMetrics.IDPMetrics().CountRequestStatusError()
 		}
+		_, _ = io.Copy(io.Discard, resp.Body)
+		_ = resp.Body.Close()
 		return nil, fmt.Errorf("unable to get logto token, statusCode %d", resp.StatusCode)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// requestJWTToken performs request to get jwt token.
// LogTo uses Basic Authentication in header (not client_id/client_secret in body like Keycloak).
func (lc *LogtoCredentials) requestJWTToken(ctx context.Context) (*http.Response, error) {
// Basic auth: base64(appId:appSecret)
auth := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", lc.clientConfig.ClientID, lc.clientConfig.ClientSecret)),
)
data := url.Values{}
data.Set("grant_type", lc.clientConfig.GrantType)
data.Set("resource", lc.clientConfig.Resource) // Required parameter for LogTo
data.Set("scope", "all")
payload := strings.NewReader(data.Encode())
req, err := http.NewRequest(http.MethodPost, lc.clientConfig.TokenEndpoint, payload)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Basic "+auth) // Basic auth in header
req.Header.Add("content-type", "application/x-www-form-urlencoded")
log.WithContext(ctx).Debug("requesting new jwt token for logto idp manager")
resp, err := lc.httpClient.Do(req)
if err != nil {
if lc.appMetrics != nil {
lc.appMetrics.IDPMetrics().CountRequestError()
}
return nil, err
}
if resp.StatusCode != http.StatusOK {
if lc.appMetrics != nil {
lc.appMetrics.IDPMetrics().CountRequestStatusError()
}
return nil, fmt.Errorf("unable to get logto token, statusCode %d", resp.StatusCode)
}
return resp, nil
// requestJWTToken performs request to get jwt token.
// LogTo uses Basic Authentication in header (not client_id/client_secret in body like Keycloak).
func (lc *LogtoCredentials) requestJWTToken(ctx context.Context) (*http.Response, error) {
// Basic auth: base64(appId:appSecret)
auth := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", lc.clientConfig.ClientID, lc.clientConfig.ClientSecret)),
)
data := url.Values{}
data.Set("grant_type", lc.clientConfig.GrantType)
data.Set("resource", lc.clientConfig.Resource) // Required parameter for LogTo
data.Set("scope", "all")
payload := strings.NewReader(data.Encode())
req, err := http.NewRequest(http.MethodPost, lc.clientConfig.TokenEndpoint, payload)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Basic "+auth) // Basic auth in header
req.Header.Add("content-type", "application/x-www-form-urlencoded")
log.WithContext(ctx).Debug("requesting new jwt token for logto idp manager")
resp, err := lc.httpClient.Do(req)
if err != nil {
if lc.appMetrics != nil {
lc.appMetrics.IDPMetrics().CountRequestError()
}
return nil, err
}
if resp.StatusCode != http.StatusOK {
if lc.appMetrics != nil {
lc.appMetrics.IDPMetrics().CountRequestStatusError()
}
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
return nil, fmt.Errorf("unable to get logto token, statusCode %d", resp.StatusCode)
}
return resp, nil
🤖 Prompt for AI Agents
In `@management/server/idp/logto.go` around lines 138 - 177, In requestJWTToken,
the http response body is leaked on non-200 paths; after lc.httpClient.Do(req)
succeeds and before returning an error when resp.StatusCode != http.StatusOK,
ensure you close (and ideally drain) resp.Body (e.g., io.Copy(io.Discard,
resp.Body) then resp.Body.Close()) so the HTTP transport connection can be
reused; leave the body open only when returning the successful resp to the
caller.

Comment on lines 180 to 210
// parseRequestJWTResponse parses jwt raw response body and extracts token and expires in seconds.
func (lc *LogtoCredentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JWTToken, error) {
jwtToken := JWTToken{}
body, err := io.ReadAll(rawBody)
if err != nil {
return jwtToken, err
}

err = lc.helper.Unmarshal(body, &jwtToken)
if err != nil {
return jwtToken, err
}

if jwtToken.ExpiresIn == 0 && jwtToken.AccessToken == "" {
return jwtToken, fmt.Errorf("error while reading response body, expires_in: %d and access_token: %s", jwtToken.ExpiresIn, jwtToken.AccessToken)
}

data, err := base64.RawURLEncoding.DecodeString(strings.Split(jwtToken.AccessToken, ".")[1])
if err != nil {
return jwtToken, err
}

// Exp maps into exp from jwt token
var IssuedAt struct{ Exp int64 }
err = lc.helper.Unmarshal(data, &IssuedAt)
if err != nil {
return jwtToken, err
}
jwtToken.expiresInTime = time.Unix(IssuedAt.Exp, 0)

return jwtToken, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against malformed JWTs to avoid panic.
strings.Split(...)[1] panics if the token doesn’t contain a payload segment. Add a length check before indexing.

🛡️ Suggested fix
-	data, err := base64.RawURLEncoding.DecodeString(strings.Split(jwtToken.AccessToken, ".")[1])
+	parts := strings.Split(jwtToken.AccessToken, ".")
+	if len(parts) < 2 {
+		return jwtToken, fmt.Errorf("invalid access token format")
+	}
+	data, err := base64.RawURLEncoding.DecodeString(parts[1])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// parseRequestJWTResponse parses jwt raw response body and extracts token and expires in seconds.
func (lc *LogtoCredentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JWTToken, error) {
jwtToken := JWTToken{}
body, err := io.ReadAll(rawBody)
if err != nil {
return jwtToken, err
}
err = lc.helper.Unmarshal(body, &jwtToken)
if err != nil {
return jwtToken, err
}
if jwtToken.ExpiresIn == 0 && jwtToken.AccessToken == "" {
return jwtToken, fmt.Errorf("error while reading response body, expires_in: %d and access_token: %s", jwtToken.ExpiresIn, jwtToken.AccessToken)
}
data, err := base64.RawURLEncoding.DecodeString(strings.Split(jwtToken.AccessToken, ".")[1])
if err != nil {
return jwtToken, err
}
// Exp maps into exp from jwt token
var IssuedAt struct{ Exp int64 }
err = lc.helper.Unmarshal(data, &IssuedAt)
if err != nil {
return jwtToken, err
}
jwtToken.expiresInTime = time.Unix(IssuedAt.Exp, 0)
return jwtToken, nil
// parseRequestJWTResponse parses jwt raw response body and extracts token and expires in seconds.
func (lc *LogtoCredentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JWTToken, error) {
jwtToken := JWTToken{}
body, err := io.ReadAll(rawBody)
if err != nil {
return jwtToken, err
}
err = lc.helper.Unmarshal(body, &jwtToken)
if err != nil {
return jwtToken, err
}
if jwtToken.ExpiresIn == 0 && jwtToken.AccessToken == "" {
return jwtToken, fmt.Errorf("error while reading response body, expires_in: %d and access_token: %s", jwtToken.ExpiresIn, jwtToken.AccessToken)
}
parts := strings.Split(jwtToken.AccessToken, ".")
if len(parts) < 2 {
return jwtToken, fmt.Errorf("invalid access token format")
}
data, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return jwtToken, err
}
// Exp maps into exp from jwt token
var IssuedAt struct{ Exp int64 }
err = lc.helper.Unmarshal(data, &IssuedAt)
if err != nil {
return jwtToken, err
}
jwtToken.expiresInTime = time.Unix(IssuedAt.Exp, 0)
return jwtToken, nil
🤖 Prompt for AI Agents
In `@management/server/idp/logto.go` around lines 180 - 210, In
parseRequestJWTResponse, guard against malformed JWTs before indexing into the
dot-split payload: after obtaining jwtToken.AccessToken call
strings.Split(jwtToken.AccessToken, ".") and verify the resulting slice has at
least 2 (preferably 3) parts; if not, return a descriptive error instead of
indexing [1]; keep existing base64.RawURLEncoding.DecodeString and Unmarshal
logic but only run them after this length check so JWTToken and IssuedAt
handling won’t panic on invalid tokens.

jpmchia and others added 2 commits January 21, 2026 03:13
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
3 New issues
3 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@mlsmaycon
Copy link
Collaborator

@jpmchia have you evaluated logto with the new embedded IdP? You can use the generic OIDC flow: https://docs.netbird.io/selfhosted/identity-providers#adding-an-identity-provider

There are a few advantages for that, because we don't need an IdP manager in this mode

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.

4 participants