Skip to content

[PM-43467] fix: Send Device-Identifier header on unauthenticated requests - #7490

Open
aj-rosado wants to merge 1 commit into
mainfrom
PM-43467/add-device-identifier-unauthenticated-requests-header
Open

aj-rosado wants to merge 1 commit into
mainfrom
PM-43467/add-device-identifier-unauthenticated-requests-header

Conversation

@aj-rosado

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-43467

📔 Objective

Server scopes OTPs (new device, 2FA email) to Device-Identifier header. Android didn't send it on resend-new-device-otp / send-email-login.

  • New DeviceIdentifierInterceptor: adds Device-Identifier when request has no Authorization (same rule as web clients). Covers future calls too.
  • Added to unauthenticated and authenticated clients. Authenticated needed because GET /config uses it and is also called pre-login with no token (server uses the device ID for feature-flag targeting). Interceptor runs after AuthTokenManager, so calls with a token are untouched.
  • Not on fill-assist / static clients, so no device ID to third-party hosts.
  • Removed DeviceIdentifier from ResendEmailRequestJson body and redundant per-call header on createAuthRequest.
  • Kept (server still needs): X-Device-Identifier on knowndevice, /connect/token form field, /auth-requests body deviceId, header on createAdminAuthRequest.

After release, update PM-43467 to start the 3-release deprecation countdown.

🤖 AI-assisted review

  • Standard: code-review-local on full branch diff vs origin/HEAD. Approved, no findings, none deferred.
  • Limitation: couldn't confirm from this repo that server reads resend-email device ID from the header (PM-43940 server change not available).

@aj-rosado aj-rosado added the ai-review Request a Claude code review label Oct 16, 2026
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:bug Change Type - Bug labels Oct 16, 2026
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the new DeviceIdentifierInterceptor, where it is wired into the authenticated and unauthenticated Retrofit clients, and the removal of the per-call Device-Identifier header on createAuthRequest and of the DeviceIdentifier body field on ResendEmailRequestJson. The authenticated client applies AuthTokenManager before the new interceptor, so requests that carry a token are left alone. Unauthenticated calls that set Authorization themselves (key connector, accounts) are skipped too, and the fill-assist, external, and static clients do not get the header. The header value (appIdProvider.uniqueAppId) matches the authDiskSource.uniqueAppId that callers passed before, and the tests cover the interceptor's branches and its wiring into each client type.

@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.80%. Comparing base (1470154) to head (49ce52a).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7490   +/-   ##
=======================================
  Coverage   86.80%   86.80%           
=======================================
  Files         895      896    +1     
  Lines       65684    65697   +13     
  Branches     9385     9386    +1     
=======================================
+ Hits        57015    57028   +13     
  Misses       5189     5189           
  Partials     3480     3480           
Flag Coverage Δ
app-data 18.64% <0.00%> (-0.01%) ⬇️
app-ui-auth-tools 18.68% <0.00%> (-0.01%) ⬇️
app-ui-platform 16.48% <0.00%> (-0.01%) ⬇️
app-ui-vault 26.88% <0.00%> (-0.01%) ⬇️
authenticator 6.03% <0.00%> (-0.01%) ⬇️
lib-core-network-bridge 4.13% <100.00%> (+0.02%) ⬆️
lib-data-ui 1.25% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aj-rosado aj-rosado added t:bug Change Type - Bug and removed t:bug Change Type - Bug labels Oct 16, 2026
@aj-rosado
aj-rosado marked this pull request as ready for review October 16, 2026 14:09
@aj-rosado
aj-rosado requested review from a team and david-livefront as code owners October 16, 2026 14:09
.client(
authenticatedOkHttpClient
.newBuilder()
.addInterceptor(deviceIdentifierInterceptor)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why add this, I thought it was only needed for unauthenticated requests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Config call goes through the authentaticated requests even before the user is authenticated.
Despite the task do not requiring it specifically for this method, I have checked what happens on the web client and they do have the device-identifier parameter on the config call.

To keep parity with other clients we need this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Makes sense

*/
@POST("/auth-requests")
suspend fun createAuthRequest(
@Header("Device-Identifier") deviceIdentifier: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see that @GET("/devices/knowndevice") is using @Header(value = "X-Device-Identifier"), should that be updated?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That is a different parameter that exists specifically on known device at server side

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Gotcha, thanks for checking!

.client(
baseOkHttpClient
.newBuilder()
.addInterceptor(deviceIdentifierInterceptor)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I assume that external retrofit instances don't need this (fill assist) but what about the static ones?

This does not cover that

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It might be worth adding something like this:

    private val unauthenticatedOkHttpClient: OkHttpClient by lazy {
        baseOkHttpClient
            .newBuilder()
            .addInterceptor(deviceIdentifierInterceptor)
            .build()
    }

And then use it in the appropriate places

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤔 are not all our static calls external requests?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ahh, good point

.client(
baseOkHttpClient
.newBuilder()
.addInterceptor(deviceIdentifierInterceptor)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It might be worth adding something like this:

    private val unauthenticatedOkHttpClient: OkHttpClient by lazy {
        baseOkHttpClient
            .newBuilder()
            .addInterceptor(deviceIdentifierInterceptor)
            .build()
    }

And then use it in the appropriate places

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.header(name = HEADER_KEY_AUTHORIZATION) != null) {
return chain.proceed(request = request)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think you need this if we only apply this interceptor to the appropriate instances of Retrofit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Based on the other discussions, we do still need this.

My last question would be, could we just send the header on all requests?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:bug Change Type - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants