> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Approve a CLI login and issue an authorization code

> Approve a client to act as you, and receive the redirect that hands it an
authorization code.

**This is the console's endpoint, not a client's.** It is called by the
Basaltic console's consent page on behalf of a signed-in user; the CLI
never calls it. A CLI opens a browser at that page, and the page calls
this. Anything driving it directly would need the user's console session,
at which point it already has everything the code would grant.

It is the half of the authorization-code flow that establishes WHO is
approving. The user must already be signed in — including any second
factor — and must be a member of the organization named in
`organization_id`. The organization is explicit rather than inferred: a
person in several has no single obvious answer, and choosing one for them
would scope the resulting token to something they did not pick.

Unlike the token endpoint, this answers in the usual API envelope. It is
not part of the surface a third-party OAuth client talks to, so it
follows the caller — and the caller is our own front end.




## OpenAPI

````yaml /api-reference/specs/iam.yaml post /v1/oauth/authorize
openapi: 3.0.3
info:
  title: Basaltic IAM API
  version: 1.0.0
  description: |
    Identity for the platform: organizations, accounts, users, groups,
    service accounts, roles and policies, together with the sign-in flows and
    the temporary STS credentials every other Basaltic API authenticates
    against.
  contact:
    name: Basaltic Support
    email: ping@basaltic.sh
  license:
    name: Proprietary
    url: https://basaltic.sh/terms
servers:
  - url: https://iam.basaltic.sh
    description: Global API endpoint
security:
  - BearerAuth: []
  - SignatureAuth: []
paths:
  /v1/oauth/authorize:
    post:
      tags:
        - IAM
      summary: Approve a CLI login and issue an authorization code
      description: >
        Approve a client to act as you, and receive the redirect that hands it
        an

        authorization code.


        **This is the console's endpoint, not a client's.** It is called by the

        Basaltic console's consent page on behalf of a signed-in user; the CLI

        never calls it. A CLI opens a browser at that page, and the page calls

        this. Anything driving it directly would need the user's console
        session,

        at which point it already has everything the code would grant.


        It is the half of the authorization-code flow that establishes WHO is

        approving. The user must already be signed in — including any second

        factor — and must be a member of the organization named in

        `organization_id`. The organization is explicit rather than inferred: a

        person in several has no single obvious answer, and choosing one for
        them

        would scope the resulting token to something they did not pick.


        Unlike the token endpoint, this answers in the usual API envelope. It is

        not part of the surface a third-party OAuth client talks to, so it

        follows the caller — and the caller is our own front end.
      operationId: authorizeOAuthClient
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthAuthorizeRequest'
      responses:
        '200':
          description: |
            Where to send the browser next. The URL carries the authorization
            code, so treat it as a credential: it is single use, valid for five
            minutes, and must not be logged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthAuthorizeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
components:
  schemas:
    OAuthAuthorizeRequest:
      type: object
      description: |
        A signed-in user approving a client, from the console's consent page.
      required:
        - client_id
        - redirect_uri
        - code_challenge
        - code_challenge_method
        - organization_id
      properties:
        client_id:
          type: string
          description: The registered client being approved.
          example: basaltic-cli
        redirect_uri:
          type: string
          description: >
            Where to deliver the code. For the CLI this must be a loopback
            address

            with any port — `http://127.0.0.1:<port>/...` or
            `http://[::1]:<port>/...`

            (RFC 8252 section 7.3). `localhost` is refused: it is a name, and

            whatever resolves it decides where the code goes.
          example: http://127.0.0.1:53682/callback
        code_challenge:
          type: string
          description: |
            Base64url SHA-256 of the client's PKCE verifier, without padding.
          example: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
        code_challenge_method:
          type: string
          enum:
            - S256
          description: >
            S256 only. `plain` is refused rather than merely discouraged:
            whoever

            intercepts the code also saw the challenge, so a plain challenge

            protects nothing.
          example: S256
        state:
          type: string
          description: |
            Opaque value echoed back on the redirect, unchanged. The client
            generated it and compares it on return.
        organization_id:
          type: string
          format: uuid
          description: >
            Which organization the resulting session is scoped to. The user must
            be

            a member of it.
    OAuthAuthorizeResponse:
      type: object
      required:
        - redirect_to
      properties:
        redirect_to:
          type: string
          description: >
            Send the browser here. The URL carries the authorization code and
            the

            state — treat it as a credential, and do not log it.
          example: http://127.0.0.1:53682/callback?code=...&state=...
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: Error code identifying the type of error
              example: INVALID_INPUT
            message:
              type: string
              description: Human-readable error message
              example: Invalid request parameters
            request_id:
              type: string
              format: uuid
              description: Request ID for debugging
              example: 550e8400-e29b-41d4-a716-446655440000
  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INVALID_INPUT
              message: Invalid request parameters
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: Authentication required
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: ACCESS_DENIED
              message: You don't have permission to perform this action
              request_id: 550e8400-e29b-41d4-a716-446655440000
    TooManyRequests:
      description: >
        Rate limit exceeded. The budget is a fixed window counted per endpoint
        and

        per caller — the authenticated principal when the request carries

        credentials, the client IP otherwise — so one throttled endpoint never

        spends another's budget, and one tenant never spends another's.


        Wait `Retry-After` seconds, then retry. The `X-RateLimit-*` headers ride
        on

        the successful responses of a rate-limited endpoint too, so a client can

        pace itself instead of discovering the ceiling by hitting it.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: RATE_LIMITED
              message: Too many requests, please try again later
              request_id: 550e8400-e29b-41d4-a716-446655440000
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Never zero.
          required: true
          schema:
            type: integer
            minimum: 1
          example: 42
        X-RateLimit-Limit:
          description: Requests allowed per window on this endpoint.
          required: true
          schema:
            type: integer
            minimum: 1
          example: 5
        X-RateLimit-Remaining:
          description: Requests left in the current window. Always 0 on a 429.
          required: true
          schema:
            type: integer
            minimum: 0
          example: 0
        X-RateLimit-Reset:
          description: >-
            Seconds until the window resets — a duration, not a timestamp, so it
            needs no clock agreement between client and server.
          required: true
          schema:
            type: integer
            minimum: 1
          example: 42
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        An OAuth 2.0 bearer token, sent as `Authorization: Bearer <token>`.
        This is the recommended way to authenticate.

        Get one by exchanging a service account's access key pair at
        `POST /v1/oauth/token` with `grant_type=client_credentials`. It is the
        standard client-credentials grant, so any OAuth-aware library will
        obtain and refresh it for you.

        ```
        curl -s -u "$KEY_ID:$SECRET" -d grant_type=client_credentials \
          https://iam.basaltic.sh/v1/oauth/token
        ```

        Tokens last an hour by default. The same access key pair is separately
        your AWS SigV4 credential for the S3-compatible object endpoint, which
        speaks nothing else.
    SignatureAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        Request signing with an access key issued to a service account. An

        HMAC-SHA256 over a canonical form of the request, close to AWS SigV4.

        The `basaltic` CLI signs for you.


        Send `Authorization`, `X-Date` (UTC, `YYYYMMDDTHHMMSSZ`) and `X-Nonce`

        (random per request); add `X-Content-Sha256` to bind a body, and

        `X-Amz-Security-Token` when using temporary credentials.


        ```

        Authorization: BASALTIC-HMAC-SHA256
        Credential=<access_key_id>/<date>/<region>/basaltic/basaltic_request,
        SignedHeaders=host;x-date;x-nonce, Signature=<hex>

        ```


        `<region>` is the region code you are calling, or `global` for the
        global

        services. A signature is valid for 5 minutes from `X-Date`, and mutating

        requests are replay-guarded on the nonce.


        **Full signing procedure, including a working implementation:**

        https://docs.basaltic.sh/authentication


        ## Rate limits

        There is no global request budget. A limit applies only where an

        operation documents a `429`, and that operation says what it counts.

        Those responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`,

        `X-RateLimit-Reset` and, on a `429`, `Retry-After` — read them rather

        than hard-coding a number. Retrying before `Retry-After` is refused and

        extends the window. Everything else is bounded by quota, not by request

        rate.

````