{
  "openapi": "3.0.3",
  "info": {
    "title": "Smart City Program SSO API",
    "version": "1.0.0",
    "description": "Implement on each program backend, not the central identity host. Replace the illustrative server URL. OAuth client credentials and central tokens stay on that backend. App handoff codes are one-use and PKCE-bound."
  },
  "servers": [
    {
      "url": "https://api.new-program.example"
    }
  ],
  "paths": {
    "/api/v1/sso/config/": {
      "get": {
        "operationId": "getProgramIdentityConfiguration",
        "security": [],
        "summary": "Read public SSO URLs",
        "responses": {
          "200": {
            "description": "Empty URLs when SSO disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "account_url": {
                      "type": "string"
                    },
                    "registration_url": {
                      "type": "string"
                    },
                    "recovery_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "enabled",
                    "account_url",
                    "registration_url",
                    "recovery_url"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sso/begin/": {
      "post": {
        "operationId": "beginProgramSignIn",
        "summary": "Begin sign-in or explicit account linking",
        "security": [
          {},
          {
            "localBearer": []
          }
        ],
        "description": "return_uri must exactly match the server allowlist. code_challenge is app-generated S256. link=true requires an authenticated existing citizen; an email match alone cannot link accounts. Pending flow expires after 10 minutes. 30 anonymous requests/minute.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Begin"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Navigate the browser to this URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authorization_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "authorization_url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid challenge or return URI.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "401": {
            "description": "Linking requires a valid citizen account; some DRF hosts return 403.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "403": {
            "description": "Linking authentication denied on hosts without an authentication challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "503": {
            "description": "SSO disabled/unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sso/callback/": {
      "get": {
        "operationId": "completeProviderCallback",
        "security": [],
        "summary": "Backend callback for the central authorization redirect",
        "description": "Validates state, code, PKCE and ID token; then provisions a citizen. Redirects to the previously allowlisted app URI with sso_code (90 seconds, single use) or sso_error=cancelled|existing_account|unavailable. Never redirects OAuth tokens.",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iss",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "error",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "App return URI with handoff or error.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "State invalid, expired or already consumed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "503": {
            "description": "SSO disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sso/exchange/": {
      "post": {
        "operationId": "exchangeProgramHandoff",
        "security": [],
        "summary": "Exchange a one-use app handoff for local tokens",
        "description": "App supplies the original verifier. Wrong, reused or expired handoffs are rejected. 30 anonymous requests/minute. Preserve auth_version and identity_session when refreshing local tokens.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Exchange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Program API access/refresh tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalTokens"
                }
              }
            }
          },
          "400": {
            "description": "Invalid sign-in proof format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "401": {
            "description": "Expired/replayed/wrong proof or inactive citizen session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "503": {
            "description": "Identity outage; allow retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sso/status/": {
      "get": {
        "operationId": "getProgramLinkStatus",
        "security": [
          {
            "localBearer": []
          }
        ],
        "summary": "Read current citizen link status",
        "responses": {
          "200": {
            "description": "Linked state and central account URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "linked": {
                      "type": "boolean"
                    },
                    "account_url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "linked",
                    "account_url"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Local session unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "503": {
            "description": "Identity validation unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sso/logout/": {
      "post": {
        "operationId": "endProgramIdentitySession",
        "security": [
          {
            "localBearer": []
          }
        ],
        "summary": "End the current program-side session",
        "description": "Clear app credentials after success. Does not end central browser sessions or other programs.",
        "responses": {
          "204": {
            "description": "Program-side session ended; no response body."
          },
          "401": {
            "description": "Local session unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          },
          "503": {
            "description": "Identity validation unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Detail"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Detail": {
        "type": "object",
        "properties": {
          "detail": {
            "type": "string"
          }
        },
        "required": [
          "detail"
        ]
      },
      "OAuthError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "error_description": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Begin": {
        "type": "object",
        "properties": {
          "return_uri": {
            "type": "string"
          },
          "code_challenge": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{43}$"
          },
          "link": {
            "type": "boolean",
            "default": false
          }
        },
        "required": [
          "return_uri",
          "code_challenge"
        ]
      },
      "Exchange": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "maxLength": 200
          },
          "code_verifier": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._~-]{43,128}$"
          }
        },
        "required": [
          "code",
          "code_verifier"
        ]
      },
      "LocalTokens": {
        "type": "object",
        "properties": {
          "access": {
            "type": "string"
          },
          "refresh": {
            "type": "string"
          }
        },
        "required": [
          "access",
          "refresh"
        ]
      }
    },
    "securitySchemes": {
      "localBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "This program's access token; not the central OAuth token."
      }
    }
  }
}