Skip to main content

Authentication and Session

The tk-ota-push CLI implements a Web-to-CLI authentication flow secured by PKCE (Proof Key for Code Exchange, S256) in AuthManager (lib/src/cli/auth/auth_manager.dart) and persists credentials locally via SessionManager (lib/src/cli/auth/session_manager.dart). For headless CI/CD environments, the OTA_API_KEY environment variable bypasses the interactive browser flow.

1. Interactive Login (tk-ota-push login)​

# Standard login (reads web_console_url from shorebird.yaml or defaults to http://localhost:3000)
tk-ota-push login

# Login with an explicit Web Console URL
tk-ota-push login --web-url https://console.ota.example.org

Web URL Resolution Cascade​

When tk-ota-push login is invoked, the Web Console URL is resolved in the following order:

  1. --web-url CLI flag
  2. web_console_url field in shorebird.yaml
  3. OTA_WEB_CONSOLE_URL environment variable
  4. Default: http://localhost:3000

Detailed PKCE Flow Sequence (AuthManager.login)​

CLI Authentication Flow
  1. Initialization: The CLI starts a local loopback listener (http://127.0.0.1:<port>).
  2. Browser Login: The CLI opens your default browser at /cli/login in the TkushPatch Console.
  3. Token Exchange: Upon successful login, a secure session token is sent back to the loopback listener.
  4. Credential Storage: Session credentials are encrypted and stored in ~/.tk_ota_push/credentials.json.

Exact query parameters sent in the initial authorization URL opened in the browser:

  • callback_url: http://127.0.0.1:<port>/callback
  • state: 32-character cryptographically random string (Random.secure())
  • code_challenge: SHA-256 digest encoded in Base64URL without = padding
  • code_challenge_method: S256
  • device_name: Platform.localHostname
  • cli_version: 0.2.0

The maximum wait timeout before the login attempt expires is 3 minutes.

2. Token Exchange and Refresh​

Initial Token Exchange (/api/cli/token/exchange)​

Once the temporary code is received on /callback, the CLI sends a POST request to <webConsoleUrl>/api/cli/token/exchange:

{
"code": "<ephemeral_code>",
"code_verifier": "<64_char_verifier>"
}

Automatic Refresh (ensureFreshToken)​

Before executing release, patch, promote, or symbols, if OTA_API_KEY is not set in the environment, the CLI loads the local session and checks credentials.isExpired:

  • A token is considered expired 30 seconds before its expires_at timestamp (DateTime.now().isAfter(expiresAt.subtract(const Duration(seconds: 30)))).
  • If expired and a refreshToken is present, AuthManager.ensureFreshToken sends a POST request to tokenRefreshUrl (or <webConsoleUrl>/api/cli/token/refresh):
{
"refresh_token": "<refresh_token>",
"tenant_id": "<tenant_id>"
}

If the server responds with 200 OK, the refreshed credentials are immediately saved to ~/.tk_ota_push/credentials.json.

3. Local Session Storage (SessionManager)​

The session file is stored at:

  • macOS / Linux: $HOME/.tk_ota_push/credentials.json (with permissions restricted via chmod 600)
  • Windows: %USERPROFILE%\.tk_ota_push\credentials.json

JSON structure persisted by CliCredentials.toJson():

{
"access_token": "<jwt_access_token>",
"refresh_token": "<jwt_refresh_token>",
"expires_at": "2026-11-09T14:30:00.000Z",
"tenant_id": "sara-money",
"username": "alice.dev@saramoney.cm",
"sub": "user-uuid-999",
"roles": ["RELEASE_DEVELOPER", "SECURITY_OFFICER"],
"token_refresh_url": "http://localhost:3000/api/cli/token/refresh",
"web_console_url": "http://localhost:3000"
}

4. Inspecting the Active Session (tk-ota-push whoami)​

The whoami command reads ~/.tk_ota_push/credentials.json and displays identity details without making any network request:

tk-ota-push whoami

Output:

Connecté en tant que : alice.dev@saramoney.cm
Tenant ID : sara-money
Subject (UUID) : user-uuid-999
Rôles : RELEASE_DEVELOPER, SECURITY_OFFICER
Expiration jeton : 2026-11-09 15:30:00.000

5. Headless CI/CD Authentication (OTA_API_KEY)​

On a continuous integration server (GitHub Actions, GitLab CI, Jenkins, Bitbucket Pipelines), do not run tk-ota-push login. Instead, inject the access token via the OTA_API_KEY environment variable:

export OTA_API_KEY="<YOUR_CI_OR_BEARER_TOKEN>"
export OTA_SERVER_URL="https://ota.example.org"
export OTA_TENANT_ID="sara-money"

tk-ota-push release --platform android --artifact aab

In OtaApiClient, whenever apiKey is non-empty, the following HTTP header is attached to every request sent to /api/admin/v1/*:

Authorization: Bearer <OTA_API_KEY>

6. Logging Out (tk-ota-push logout)​

To clear the local session and delete ~/.tk_ota_push/credentials.json:

tk-ota-push logout

Possible outputs:

  • Session supprimée avec succès. (if a session file existed)
  • Déjà déconnecté. (if no credentials.json file was present)