diff --git a/.github/conformance/config.template.json b/.github/conformance/config.template.json new file mode 100644 index 0000000..8931fd5 --- /dev/null +++ b/.github/conformance/config.template.json @@ -0,0 +1,11 @@ +{ + "alias": "portal-oidc", + "description": "portal-oidc OIDC conformance test", + "server": { + "discoveryUrl": "${DISCOVERY_URL}" + }, + "client": { + "client_id": "${CLIENT_ID}", + "client_secret": "${CLIENT_SECRET}" + } +} diff --git a/.github/conformance/docker-compose.yml b/.github/conformance/docker-compose.yml new file mode 100644 index 0000000..42f330c --- /dev/null +++ b/.github/conformance/docker-compose.yml @@ -0,0 +1,27 @@ +services: + mongodb: + image: mongo:6.0.13 + httpd: + image: ${CONFORMANCE_HTTPD_IMAGE} + ports: + - "8443:8443" + depends_on: + - server + server: + image: ${CONFORMANCE_SERVER_IMAGE} + command: > + java + -Djdk.tls.maxHandshakeMessageSize=65536 + -jar /server/fapi-test-suite.jar + --fintechlabs.base_url=https://localhost.emobix.co.uk:8443 + --fintechlabs.devmode=true + --fintechlabs.startredir=true + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - mongodb + logging: + driver: json-file + options: + max-size: "500k" + max-file: "5" diff --git a/.github/conformance/expected-skips.json b/.github/conformance/expected-skips.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/.github/conformance/expected-skips.json @@ -0,0 +1 @@ +[] diff --git a/.github/conformance/run.sh b/.github/conformance/run.sh new file mode 100755 index 0000000..cf97cb5 --- /dev/null +++ b/.github/conformance/run.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +PORTAL_OIDC_URL="${PORTAL_OIDC_URL:-http://localhost:8080}" +CONFORMANCE_SERVER="${CONFORMANCE_SERVER:-https://localhost:8443}" +CONFORMANCE_TOKEN="${CONFORMANCE_TOKEN-}" +DISCOVERY_URL="${DISCOVERY_URL:-http://host.docker.internal:8080/.well-known/openid-configuration}" +REDIRECT_URI="https://localhost.emobix.co.uk:8443/test/a/portal-oidc/callback" +OIDC_SERVER_LOCAL="${OIDC_SERVER_LOCAL:-localhost:8080}" + +TEST_PLAN="oidcc-basic-certification-test-plan" +TEST_VARIANT='{"server_metadata":"discovery","client_registration":"static_client"}' + +mkdir -p "$SCRIPT_DIR/results" + +echo "==> Creating test client..." +RESPONSE=$(curl -sf -X POST "$PORTAL_OIDC_URL/api/v1/admin/clients" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"conformance-suite\", + \"client_type\": \"confidential\", + \"redirect_uris\": [\"$REDIRECT_URI\"] + }") + +CLIENT_ID=$(echo "$RESPONSE" | jq -r '.client_id') +CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.client_secret') + +if [[ -z "$CLIENT_ID" || -z "$CLIENT_SECRET" ]]; then + echo "Error: Failed to extract client credentials from response" + exit 1 +fi + +echo " client_id=$CLIENT_ID" +echo " client_secret=***" + +echo "==> Generating test config..." +sed \ + -e "s|\${DISCOVERY_URL}|$DISCOVERY_URL|g" \ + -e "s|\${CLIENT_ID}|$CLIENT_ID|g" \ + -e "s|\${CLIENT_SECRET}|$CLIENT_SECRET|g" \ + "$SCRIPT_DIR/config.template.json" > "$SCRIPT_DIR/results/config.json" + +echo "==> Running conformance test plan: $TEST_PLAN" +python3 "$REPO_DIR/.github/scripts/run-test-plan.py" \ + --server "$CONFORMANCE_SERVER" \ + --token "$CONFORMANCE_TOKEN" \ + --plan "$TEST_PLAN" \ + --variant "$TEST_VARIANT" \ + --config "$SCRIPT_DIR/results/config.json" \ + --output "$SCRIPT_DIR/results" \ + --oidc-server "$OIDC_SERVER_LOCAL" \ + --expected-skips "$SCRIPT_DIR/expected-skips.json" + +echo "==> Done. Results saved to $SCRIPT_DIR/results/" diff --git a/.github/scripts/run-test-plan.py b/.github/scripts/run-test-plan.py new file mode 100755 index 0000000..3d0dbb1 --- /dev/null +++ b/.github/scripts/run-test-plan.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +"""Run an OpenID Connect conformance suite test plan.""" + +import argparse +import json +import os +import re +import sys +import time + +import httpx + +DEV_MODE = os.environ.get("CONFORMANCE_DEV_MODE", "0") == "1" + + +def create_api_client(base_url: str, token: str) -> httpx.Client: + headers = {} + if token: + headers["Authorization"] = f"Bearer {token}" + return httpx.Client( + base_url=base_url, + headers=headers, + verify=not DEV_MODE, + timeout=60.0, + ) + + +def create_browser_client() -> httpx.Client: + # SSL verification disabled: conformance suite uses self-signed certificates + return httpx.Client( + verify=False, + timeout=30.0, + ) + + +def create_test_plan( + client: httpx.Client, plan_name: str, variant: dict | None, config: dict +) -> dict: + params: dict[str, str] = {"planName": plan_name} + if variant: + params["variant"] = json.dumps(variant) + resp = client.post( + "/api/plan", + params=params, + json=config, + ) + resp.raise_for_status() + return resp.json() + + +def get_test_module_info(client: httpx.Client, module_id: str) -> dict: + resp = client.get(f"/api/info/{module_id}") + resp.raise_for_status() + return resp.json() + + +def start_test_module(client: httpx.Client, plan_id: str, module_name: str) -> dict: + resp = client.post( + "/api/runner", + params={"test": module_name, "plan": plan_id}, + ) + resp.raise_for_status() + return resp.json() + + +def get_test_log(client: httpx.Client, module_id: str) -> list: + resp = client.get(f"/api/log/{module_id}") + resp.raise_for_status() + return resp.json() + + +def find_authorize_url(log_entries: list) -> str | None: + for entry in log_entries: + url = entry.get("redirect_to_authorization_endpoint", "") + if url: + return url + return None + + +def perform_browser_interaction( + api_client: httpx.Client, + browser: httpx.Client, + module_id: str, + oidc_server_url: str, +) -> None: + log = get_test_log(api_client, module_id) + auth_url = find_authorize_url(log) + if not auth_url: + return + + if oidc_server_url: + auth_url = auth_url.replace("host.docker.internal:8080", oidc_server_url) + + print(" Browser: visiting authorize URL") + try: + resp = browser.get(auth_url, follow_redirects=False) + except httpx.HTTPError as e: + print(f" Browser: authorize request failed: {e}") + return + + if resp.status_code not in (301, 302, 303, 307, 308): + print(f" Browser: OP returned {resp.status_code} (no redirect)") + return + + callback_url = resp.headers.get("location", "") + if not callback_url: + print(" Browser: redirect with no location header") + return + + print(" Browser: following redirect to callback") + try: + cb_resp = browser.get(callback_url) + except httpx.HTTPError as e: + print(f" Browser: callback request failed: {e}") + return + + match = re.search(r"xhr\.open\('POST',\s*\"([^\"]+)\"", cb_resp.text) + if not match: + print(" Browser: no implicit submit URL found in callback page") + return + + implicit_url = match.group(1).replace("\\/", "/") + print(" Browser: submitting fragment to implicit endpoint") + try: + browser.post(implicit_url, content="", headers={"Content-Type": "text/plain"}) + except httpx.HTTPError as e: + print(f" Browser: implicit submit failed: {e}") + + +def wait_for_test( + api_client: httpx.Client, + browser: httpx.Client, + module_id: str, + oidc_server_url: str, + timeout: int = 60, +) -> dict: + start = time.time() + browser_tried = False + while time.time() - start < timeout: + info = get_test_module_info(api_client, module_id) + status = info.get("status", "UNKNOWN") + if status in ("FINISHED", "INTERRUPTED"): + return info + if status == "WAITING" and not browser_tried: + browser_tried = True + perform_browser_interaction( + api_client, browser, module_id, oidc_server_url + ) + time.sleep(2) + raise TimeoutError(f"Test {module_id} did not finish within {timeout}s") + + +def load_expected_skips(path: str | None) -> set[str]: + if not path or not os.path.exists(path): + return set() + with open(path) as f: + entries = json.load(f) + return {e["test_module"] for e in entries} + + +def run_plan( + api_client: httpx.Client, + browser: httpx.Client, + plan_name: str, + variant: dict | None, + config: dict, + output_dir: str, + oidc_server_url: str, + expected_skips: set[str] | None = None, +) -> bool: + print(f"Creating test plan: {plan_name}") + plan = create_test_plan(api_client, plan_name, variant, config) + plan_id = plan["id"] + modules = plan.get("modules", []) + print(f"Plan ID: {plan_id}") + print(f"Modules to run: {len(modules)}") + + all_passed = True + results = [] + + skips = expected_skips or set() + + for module_entry in modules: + module_name = module_entry["testModule"] + if module_name in skips: + print(f"\n--- Skipping: {module_name} (expected skip) ---") + results.append({"module": module_name, "result": "SKIPPED"}) + continue + print(f"\n--- Running: {module_name} ---") + + started = start_test_module(api_client, plan_id, module_name) + module_id = started["id"] + print(f"Module ID: {module_id}") + + try: + info = wait_for_test( + api_client, browser, module_id, oidc_server_url + ) + except TimeoutError as e: + print(f"TIMEOUT: {e}") + all_passed = False + results.append({"module": module_name, "result": "TIMEOUT"}) + continue + + result = info.get("result", "UNKNOWN") + print(f"Result: {result}") + + log = get_test_log(api_client, module_id) + log_path = os.path.join(output_dir, f"{module_name}.json") + with open(log_path, "w") as f: + json.dump(log, f, indent=2) + + results.append({"module": module_name, "result": result}) + + if result not in ("PASSED", "WARNING", "REVIEW", "SKIPPED"): + all_passed = False + + summary_path = os.path.join(output_dir, "summary.json") + with open(summary_path, "w") as f: + json.dump( + {"plan_id": plan_id, "plan_name": plan_name, "results": results}, + f, + indent=2, + ) + + print("\n=== Summary ===") + passed_count = 0 + failed_count = 0 + skipped_count = 0 + for r in results: + if r["result"] == "SKIPPED": + status_mark = "SKIP" + skipped_count += 1 + elif r["result"] in ("PASSED", "WARNING", "REVIEW"): + status_mark = "PASS" + passed_count += 1 + else: + status_mark = "FAIL" + failed_count += 1 + print(f" [{status_mark}] {r['module']}: {r['result']}") + + total = len(results) + print(f"\n Total: {total} Passed: {passed_count} Failed: {failed_count} Skipped: {skipped_count}") + + return all_passed + + +def main() -> None: + parser = argparse.ArgumentParser(description="Run OIDC conformance test plan") + parser.add_argument("--server", required=True, help="Conformance suite base URL") + parser.add_argument("--token", default="", help="API bearer token") + parser.add_argument("--plan", required=True, help="Test plan name") + parser.add_argument("--variant", default=None, help="Variant selection as JSON") + parser.add_argument("--config", required=True, help="Path to test config JSON") + parser.add_argument("--output", required=True, help="Output directory for results") + parser.add_argument( + "--oidc-server", default="", + help="Local OIDC server host:port for URL rewriting (e.g., localhost:8080)", + ) + parser.add_argument( + "--expected-skips", default=None, + help="Path to JSON file listing test modules to skip", + ) + args = parser.parse_args() + + with open(args.config) as f: + config = json.load(f) + + variant = json.loads(args.variant) if args.variant else None + skips = load_expected_skips(args.expected_skips) + + os.makedirs(args.output, exist_ok=True) + + api_client = create_api_client(args.server, args.token) + browser = create_browser_client() + + try: + passed = run_plan( + api_client, browser, args.plan, variant, config, args.output, + args.oidc_server, skips, + ) + finally: + api_client.close() + browser.close() + + if not passed: + print("\nSome tests failed.") + sys.exit(1) + + print("\nAll tests passed.") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/build-conformance-suite.yaml b/.github/workflows/build-conformance-suite.yaml new file mode 100644 index 0000000..f1006c8 --- /dev/null +++ b/.github/workflows/build-conformance-suite.yaml @@ -0,0 +1,62 @@ +name: Build Conformance Suite Images + +on: + workflow_dispatch: + inputs: + tag: + description: "Conformance suite release tag (e.g. release-v5.1.35)" + required: true + default: "release-v5.1.35" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_PREFIX: ghcr.io/traptitech/portal-oidc/conformance + +jobs: + build: + name: Build and Push + runs-on: ubuntu-latest + steps: + - name: Clone conformance suite + run: | + git clone --depth 1 --branch "${{ inputs.tag }}" \ + https://gitlab.com/openid/conformance-suite.git + + - name: Build with Maven + working-directory: conformance-suite + env: + MAVEN_CACHE: /tmp/m2 + run: | + mkdir -p "$MAVEN_CACHE" + docker compose -f builder-compose.yml run --rm builder + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push httpd + run: | + docker build \ + -t "$IMAGE_PREFIX-httpd:${{ inputs.tag }}" \ + conformance-suite/httpd + docker push "$IMAGE_PREFIX-httpd:${{ inputs.tag }}" + + - name: Build and push server + run: | + cat > /tmp/Dockerfile <<'EOF' + FROM eclipse-temurin:17 + RUN apt-get update && apt-get install -y redir && rm -rf /var/lib/apt/lists/* + COPY fapi-test-suite.jar /server/fapi-test-suite.jar + EOF + cp conformance-suite/target/fapi-test-suite.jar /tmp/ + docker build \ + -t "$IMAGE_PREFIX-server:${{ inputs.tag }}" \ + /tmp + docker push "$IMAGE_PREFIX-server:${{ inputs.tag }}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e084f8..8cb0334 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -101,11 +101,12 @@ jobs: files: coverage.out token: ${{ secrets.CODECOV_TOKEN }} - vulnerability: - name: Vulnerability Check + security: + name: Security Scan runs-on: ubuntu-latest permissions: contents: read + security-events: write steps: - uses: actions/checkout@v6 @@ -118,30 +119,11 @@ jobs: go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... - security: - name: Security Scan - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-go@v6 - with: - go-version-file: ${{ env.GO_VERSION_FILE }} - - name: Run Gosec Security Scanner uses: securego/gosec@master with: args: -exclude-generated -fmt sarif -out gosec.sarif ./... - - name: Upload Gosec SARIF - uses: github/codeql-action/upload-sarif@v4 - if: always() && hashFiles('gosec.sarif') != '' - with: - sarif_file: gosec.sarif - - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.33.1 with: @@ -150,8 +132,15 @@ jobs: output: 'trivy.sarif' exit-code: '0' - - name: Upload Trivy SARIF + - name: Merge SARIF results + if: always() + run: | + jq -s '{ "$schema": .[0]["$schema"], version: .[0].version, runs: [.[].runs[]] }' \ + gosec.sarif trivy.sarif > security.sarif + + - name: Upload SARIF uses: github/codeql-action/upload-sarif@v4 - if: always() && hashFiles('trivy.sarif') != '' + if: always() && hashFiles('security.sarif') != '' with: - sarif_file: trivy.sarif + sarif_file: security.sarif + category: security diff --git a/.github/workflows/conformance.yaml b/.github/workflows/conformance.yaml new file mode 100644 index 0000000..9e5640d --- /dev/null +++ b/.github/workflows/conformance.yaml @@ -0,0 +1,145 @@ +name: OIDC Conformance Suite + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: read + +env: + GO_VERSION_FILE: go.mod + CONFORMANCE_HTTPD_IMAGE: ghcr.io/traptitech/portal-oidc/conformance-httpd:release-v5.1.35 + CONFORMANCE_SERVER_IMAGE: ghcr.io/traptitech/portal-oidc/conformance-server:release-v5.1.35 + +jobs: + conformance: + name: Conformance Suite + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + services: + oidc-db: + image: mariadb:10.11.15 + env: + MARIADB_ROOT_PASSWORD: password + MARIADB_DATABASE: oidc + ports: + - 3307:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 + with: + go-version-file: ${{ env.GO_VERSION_FILE }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Python dependencies + run: pip install httpx + + - name: Apply DB schema + run: mysql -h 127.0.0.1 -P 3307 -u root oidc < db/schema.sql + env: + MYSQL_PWD: password + + - name: Build portal-oidc + run: go build -o portal-oidc ./cmd/ + + - name: Generate RSA key + run: | + mkdir -p data + openssl genpkey -algorithm RSA -out data/private.pem -pkeyopt rsa_keygen_bits:2048 + + - name: Start portal-oidc + run: | + ./portal-oidc serve > /tmp/portal-oidc.log 2>&1 & + echo $! > /tmp/portal-oidc.pid + env: + OIDC_HOST: http://host.docker.internal:8080 + OIDC_DATABASE__HOST: 127.0.0.1 + OIDC_DATABASE__PORT: "3307" + OIDC_DATABASE__USER: root + OIDC_DATABASE__PASSWORD: password + OIDC_DATABASE__NAME: oidc + OIDC_OAUTH__SECRET: conformance-test-secret-key-32!! + + - name: Wait for portal-oidc health check + run: | + for i in $(seq 1 30); do + if curl -sf http://localhost:8080/health > /dev/null 2>&1; then + echo "portal-oidc is ready" + exit 0 + fi + echo "Waiting for portal-oidc... ($i/30)" + sleep 2 + done + echo "portal-oidc failed to start" + exit 1 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Start conformance suite + run: docker compose -f .github/conformance/docker-compose.yml up -d + + - name: Wait for conformance suite + run: | + for i in $(seq 1 60); do + if curl -sfk https://localhost:8443 > /dev/null 2>&1; then + echo "Conformance suite is ready" + exit 0 + fi + echo "Waiting for conformance suite... ($i/60)" + sleep 5 + done + echo "Conformance suite failed to start" + docker compose -f .github/conformance/docker-compose.yml logs + exit 1 + + - name: Run conformance tests + working-directory: ${{ github.workspace }} + run: | + chmod +x .github/conformance/run.sh + bash .github/conformance/run.sh + env: + CONFORMANCE_SERVER: https://localhost:8443 + CONFORMANCE_TOKEN: "" + CONFORMANCE_DEV_MODE: "1" + PORTAL_OIDC_URL: http://localhost:8080 + DISCOVERY_URL: http://host.docker.internal:8080/.well-known/openid-configuration + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: conformance-results + path: .github/conformance/results/ + retention-days: 30 + + - name: Portal OIDC server logs + if: always() + run: cat /tmp/portal-oidc.log || true + + - name: Conformance suite logs + if: failure() + run: docker compose -f .github/conformance/docker-compose.yml logs --tail=200 diff --git a/.gitignore b/.gitignore index 3653d52..74faab4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ Thumbs.db # Local data (secrets, keys) data/ + +# Conformance suite results +.github/conformance/results/ diff --git a/TEST_MANUAL.md b/TEST_MANUAL.md new file mode 100644 index 0000000..101ce4d --- /dev/null +++ b/TEST_MANUAL.md @@ -0,0 +1,62 @@ +# Conformance Suite でのテストのやり方 + +## 目的 + +OpenID Connect Conformance Suite を使って本リポジトリの OIDC 実装を手動で検証する。 + +## 1. OIDC サーバーの起動 + +1. 開発環境を起動する + + ```bash + mise run dev + ``` + + `config.yaml`のhostを`http://host.docker.internal:8080` に書き換える必要があるはず。 + +## 2. Discovery の確認 + +次の URL が 200 で返ることを確認する。 + +- OpenID Provider Configuration: `http://localhost:8080/.well-known/openid-configuration` +- JWKS: `http://localhost:8080/.well-known/jwks.json` + +例: + +```bash +curl -sS http://localhost:8080/.well-known/openid-configuration | head -n 5 +curl -sS http://localhost:8080/.well-known/jwks.json | head -n 5 +``` + +## 3. Conformance Suite 用クライアントの作成 + +Conformance Suite が提示する Redirect URI を登録する。`client_type` は `confidential` を推奨。 +``の例: `https://localhost.emobix.co.uk:8443/test/a/alias/callback` <- 一度Create Test Planをすると表示されたはず。 + +```bash +curl -sS -X POST http://localhost:8080/api/v1/admin/clients \ + -H 'Content-Type: application/json' \ + -d '{"name":"conformance-suite","client_type":"confidential","redirect_uris":[""]}' +``` + +レスポンスに `client_id` と `client_secret` が含まれるので控えておく。 + +## 4. Conformance Suite の起動 + +```bash +git clone git@github.com:openid-certification/conformance-suite.git +docker compose up +``` + +## 5. Suite 側の設定 + +Suite の新規テスト作成画面で、以下を設定する。 + +- Alias: 好きな名称 +- Client ID: 手順 3 で作成した `client_id` +- Client Secret: 手順 3 で作成した `client_secret` +- Discovery URL: `http://host.docker.internal:8080/.well-known/openid-configuration` + +## 7. 実行と結果確認 + +Suite からテストを実行し、失敗したテスト項目のログを確認して修正する。 diff --git a/api/openapi.yaml b/api/openapi.yaml index f547591..5759dac 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -13,7 +13,590 @@ servers: - url: "http://localhost:8080" description: local -paths: {} +paths: + /api/v1/admin/clients: + get: + operationId: getClients + summary: クライアント一覧取得 + tags: + - clients + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Client" + "401": + description: Unauthorized + post: + operationId: createClient + summary: クライアント作成 + tags: + - clients + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ClientCreate" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/ClientWithSecret" + "400": + description: Bad Request + "401": + description: Unauthorized + + /api/v1/admin/clients/{clientId}: + parameters: + - name: clientId + in: path + required: true + schema: + type: string + format: uuid + get: + operationId: getClient + summary: クライアント取得 + tags: + - clients + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + "401": + description: Unauthorized + "404": + description: Not Found + put: + operationId: updateClient + summary: クライアント更新 + tags: + - clients + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ClientUpdate" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + "400": + description: Bad Request + "401": + description: Unauthorized + "404": + description: Not Found + delete: + operationId: deleteClient + summary: クライアント削除 + tags: + - clients + responses: + "204": + description: No Content + "401": + description: Unauthorized + "404": + description: Not Found + + /api/v1/admin/clients/{clientId}/secret: + parameters: + - name: clientId + in: path + required: true + schema: + type: string + format: uuid + post: + operationId: regenerateClientSecret + summary: クライアントシークレット再生成 + tags: + - clients + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ClientSecret" + "401": + description: Unauthorized + "404": + description: Not Found + + /.well-known/openid-configuration: + get: + operationId: getOpenIDConfiguration + summary: OpenID Provider Configuration + description: OpenID Connect Discovery 1.0 のメタデータを返却 + tags: + - discovery + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/OpenIDConfiguration" + + /.well-known/jwks.json: + get: + operationId: getJWKS + summary: JSON Web Key Set + description: トークン検証用の公開鍵セットを返却 + tags: + - discovery + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/JWKS" + + /oauth2/userinfo: + get: + operationId: getUserInfo + summary: UserInfo エンドポイント (GET) + description: アクセストークンに紐づくユーザー情報を返却 + tags: + - oauth2/oidc + security: + - bearerAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UserInfo" + "401": + description: Unauthorized + post: + operationId: postUserInfo + summary: UserInfo エンドポイント (POST) + description: アクセストークンに紐づくユーザー情報を返却 (OIDC Core 1.0 準拠) + tags: + - oauth2/oidc + security: + - bearerAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UserInfo" + "401": + description: Unauthorized + + /oauth2/authorize: + get: + operationId: authorize + summary: 認可エンドポイント + description: OAuth2/OIDC認可リクエストを処理し、ユーザー認証後にリダイレクト + tags: + - oauth2/oidc + parameters: + - name: response_type + in: query + required: true + schema: + type: string + enum: [code] + description: レスポンスタイプ (現在は code のみサポート) + - name: client_id + in: query + required: true + schema: + type: string + format: uuid + - name: redirect_uri + in: query + required: true + schema: + type: string + format: uri + - name: scope + in: query + required: false + schema: + type: string + description: スペース区切りのスコープ (openid, profile, email 等) + - name: state + in: query + required: false + schema: + type: string + description: CSRF対策用のランダム文字列 + - name: nonce + in: query + required: false + schema: + type: string + description: リプレイ攻撃対策用 (OIDC) + - name: code_challenge + in: query + required: false + schema: + type: string + description: PKCE code_challenge + - name: code_challenge_method + in: query + required: false + schema: + type: string + enum: [S256, plain] + description: PKCE code_challenge_method + responses: + "302": + description: 認可成功時はredirect_uriへリダイレクト + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/OAuthError" + + /oauth2/token: + post: + operationId: token + summary: トークンエンドポイント + description: 認可コードをトークンに交換、またはトークンをリフレッシュ + tags: + - oauth2/oidc + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/TokenRequest" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/TokenResponse" + "400": + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/OAuthError" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/OAuthError" components: - schemas: {} + schemas: + ClientType: + type: string + enum: + - public + - confidential + description: | + - public: クライアントシークレットを安全に保持できないクライアント (SPA, モバイルアプリ等) <将来実装> + - confidential: クライアントシークレットを安全に保持できるクライアント (サーバーサイドアプリ) + + Client: + type: object + required: + - client_id + - name + - client_type + - redirect_uris + - created_at + - updated_at + properties: + client_id: + type: string + format: uuid + name: + type: string + maxLength: 255 + client_type: + $ref: "#/components/schemas/ClientType" + redirect_uris: + type: array + items: + type: string + format: uri + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + ClientCreate: + type: object + required: + - name + - client_type + - redirect_uris + properties: + name: + type: string + maxLength: 255 + client_type: + $ref: "#/components/schemas/ClientType" + redirect_uris: + type: array + items: + type: string + format: uri + minItems: 1 + + ClientUpdate: + type: object + required: + - name + - client_type + - redirect_uris + properties: + name: + type: string + maxLength: 255 + client_type: + $ref: "#/components/schemas/ClientType" + redirect_uris: + type: array + items: + type: string + format: uri + minItems: 1 + + ClientWithSecret: + allOf: + - $ref: "#/components/schemas/Client" + - type: object + required: + - client_secret + properties: + client_secret: + type: string + description: クライアントシークレット (作成時のみ返却、再取得不可) + + ClientSecret: + type: object + required: + - client_secret + properties: + client_secret: + type: string + description: 新しいクライアントシークレット + + TokenRequest: + type: object + required: + - grant_type + properties: + grant_type: + type: string + enum: + - authorization_code + - refresh_token + code: + type: string + description: 認可コード (grant_type=authorization_code 時に必須) + redirect_uri: + type: string + format: uri + description: リダイレクトURI (grant_type=authorization_code 時に必須) + client_id: + type: string + format: uuid + client_secret: + type: string + description: クライアントシークレット (confidential client の場合) + refresh_token: + type: string + description: リフレッシュトークン (grant_type=refresh_token 時に必須) + code_verifier: + type: string + description: PKCE code_verifier + + TokenResponse: + type: object + required: + - access_token + - token_type + - expires_in + properties: + access_token: + type: string + token_type: + type: string + enum: [Bearer] + expires_in: + type: integer + description: アクセストークンの有効期限 (秒) + refresh_token: + type: string + id_token: + type: string + description: ID トークン (scope に openid が含まれる場合) + scope: + type: string + description: 付与されたスコープ + + OAuthError: + type: object + required: + - error + properties: + error: + type: string + enum: + - invalid_request + - invalid_client + - invalid_grant + - unauthorized_client + - unsupported_grant_type + - invalid_scope + - access_denied + - server_error + error_description: + type: string + description: エラーの詳細説明 + + OpenIDConfiguration: + type: object + required: + - issuer + - authorization_endpoint + - token_endpoint + - userinfo_endpoint + - jwks_uri + - response_types_supported + - subject_types_supported + - id_token_signing_alg_values_supported + properties: + issuer: + type: string + format: uri + authorization_endpoint: + type: string + format: uri + token_endpoint: + type: string + format: uri + userinfo_endpoint: + type: string + format: uri + jwks_uri: + type: string + format: uri + response_types_supported: + type: array + items: + type: string + subject_types_supported: + type: array + items: + type: string + id_token_signing_alg_values_supported: + type: array + items: + type: string + scopes_supported: + type: array + items: + type: string + token_endpoint_auth_methods_supported: + type: array + items: + type: string + claims_supported: + type: array + items: + type: string + code_challenge_methods_supported: + type: array + items: + type: string + + JWKS: + type: object + required: + - keys + properties: + keys: + type: array + items: + $ref: "#/components/schemas/JWK" + + JWK: + type: object + required: + - kty + - kid + - use + properties: + kty: + type: string + description: Key Type (RSA, EC 等) + kid: + type: string + description: Key ID + use: + type: string + enum: [sig, enc] + alg: + type: string + description: Algorithm (RS256 等) + n: + type: string + description: RSA modulus (Base64url) + e: + type: string + description: RSA exponent (Base64url) + + UserInfo: + type: object + required: + - sub + properties: + sub: + type: string + description: Subject Identifier + name: + type: string + preferred_username: + type: string + email: + type: string + format: email + email_verified: + type: boolean + picture: + type: string + format: uri + updated_at: + type: integer + description: Unix timestamp + + securitySchemes: + bearerAuth: + type: http + scheme: bearer diff --git a/cmd/config.go b/cmd/config.go index e7d1efa..7efdf4f 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -15,34 +15,44 @@ type Config struct { Host string `koanf:"host"` Environment string `koanf:"environment"` Database DatabaseConfig `koanf:"database"` + Portal PortalConfig `koanf:"portal"` OAuth OAuthConfig `koanf:"oauth"` } +type PortalConfig struct { + Database DatabaseConfig `koanf:"database"` +} + type DatabaseConfig struct { Host string `koanf:"host"` Port int `koanf:"port"` User string `koanf:"user"` - Password string `koanf:"password"` + Password string `koanf:"password"` // #nosec G117 -- config struct, not serialized Name string `koanf:"name"` } type OAuthConfig struct { - Secret string `koanf:"secret"` + Secret string `koanf:"secret"` // #nosec G117 -- config struct, not serialized KeyFile string `koanf:"key_file"` TestUserID string `koanf:"test_user_id"` } var defaults = map[string]any{ - "host": "http://localhost:8080", - "environment": "development", - "database.host": "localhost", - "database.port": 3307, - "database.user": "root", - "database.password": "password", - "database.name": "oidc", - "oauth.secret": "my-super-secret-signing-key-32!!", - "oauth.key_file": "data/private.pem", - "oauth.test_user_id": "testuser", + "host": "http://localhost:8080", + "environment": "development", + "database.host": "localhost", + "database.port": 3307, + "database.user": "root", + "database.password": "password", + "database.name": "oidc", + "portal.database.host": "localhost", + "portal.database.port": 3306, + "portal.database.user": "root", + "portal.database.password": "password", + "portal.database.name": "portal", + "oauth.secret": "my-super-secret-signing-key-32!!", + "oauth.key_file": "data/private.pem", + "oauth.test_user_id": "testuser", } func loadConfig(configPath string) (*Config, error) { @@ -66,7 +76,8 @@ func loadConfig(configPath string) (*Config, error) { } if err := k.Load(env.Provider("OIDC_", ".", func(s string) string { - return strings.ToLower(strings.TrimPrefix(s, "OIDC_")) + key := strings.ToLower(strings.TrimPrefix(s, "OIDC_")) + return strings.ReplaceAll(key, "__", ".") }), nil); err != nil { return nil, err } diff --git a/cmd/oauth.go b/cmd/oauth.go new file mode 100644 index 0000000..4ed022e --- /dev/null +++ b/cmd/oauth.go @@ -0,0 +1,205 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/compose" + "github.com/ory/fosite/token/jwt" +) + +type OAuthProviderConfig struct { + Issuer string + AccessTokenLifespan time.Duration + RefreshTokenLifespan time.Duration + AuthCodeLifespan time.Duration + IDTokenLifespan time.Duration + Secret []byte // #nosec G117 -- internal config, not serialized +} + +func defaultOAuthProviderConfig() OAuthProviderConfig { + return OAuthProviderConfig{ + Issuer: "http://localhost:8080", + AccessTokenLifespan: time.Hour, + RefreshTokenLifespan: 30 * 24 * time.Hour, + AuthCodeLifespan: 5 * time.Minute, + IDTokenLifespan: time.Hour, + Secret: []byte("my-super-secret-signing-key-32!!"), + } +} + +func newOAuthProvider(storage fosite.Storage, config OAuthProviderConfig, privateKey *rsa.PrivateKey) fosite.OAuth2Provider { + fositeConfig := &fosite.Config{ + AccessTokenLifespan: config.AccessTokenLifespan, + RefreshTokenLifespan: config.RefreshTokenLifespan, + AuthorizeCodeLifespan: config.AuthCodeLifespan, + IDTokenLifespan: config.IDTokenLifespan, + GlobalSecret: config.Secret, + ScopeStrategy: fosite.ExactScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + SendDebugMessagesToClients: false, + EnforcePKCE: false, + EnforcePKCEForPublicClients: true, + EnablePKCEPlainChallengeMethod: false, + AccessTokenIssuer: config.Issuer, + IDTokenIssuer: config.Issuer, + } + + privateKeyGetter := func(_ context.Context) (interface{}, error) { + return privateKey, nil + } + + return compose.Compose( + fositeConfig, + storage, + &compose.CommonStrategy{ + CoreStrategy: compose.NewOAuth2HMACStrategy(fositeConfig), + Signer: &jwt.DefaultSigner{GetPrivateKey: privateKeyGetter}, + OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(privateKeyGetter, fositeConfig), + }, + + compose.OAuth2AuthorizeExplicitFactory, + compose.OAuth2PKCEFactory, + compose.OAuth2RefreshTokenGrantFactory, + compose.OAuth2TokenIntrospectionFactory, + compose.OAuth2TokenRevocationFactory, + compose.OpenIDConnectExplicitFactory, + compose.OpenIDConnectRefreshFactory, + ) +} + +func loadOrGenerateKey(path string) (*rsa.PrivateKey, error) { + key, err := loadKey(path) + if err == nil { + return key, nil + } + + if !errors.Is(err, os.ErrNotExist) { + return nil, err + } + + key, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + if err := saveKey(path, key); err != nil { + return nil, err + } + + return key, nil +} + +func loadKey(path string) (key *rsa.PrivateKey, err error) { + root, filename, err := openKeyRoot(path) + if err != nil { + return nil, err + } + defer func() { + if cerr := root.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + f, err := root.Open(filename) + if err != nil { + return nil, err + } + defer func() { + if cerr := f.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + data, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + block, _ := pem.Decode(data) + if block == nil { + return nil, errors.New("failed to decode PEM") + } + + switch block.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(block.Bytes) + case "PRIVATE KEY": + parsed, pkcs8Err := x509.ParsePKCS8PrivateKey(block.Bytes) + if pkcs8Err != nil { + return nil, pkcs8Err + } + rsaKey, ok := parsed.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("private key is not RSA") + } + return rsaKey, nil + default: + return nil, fmt.Errorf("unsupported PEM type: %s", block.Type) + } +} + +func openKeyRoot(path string) (*os.Root, string, error) { + cleanPath := filepath.Clean(path) + dir := filepath.Dir(cleanPath) + filename := filepath.Base(cleanPath) + + root, err := os.OpenRoot(dir) + if err != nil { + return nil, "", err + } + + return root, filename, nil +} + +func saveKey(path string, key *rsa.PrivateKey) (err error) { + cleanPath := filepath.Clean(path) + dir := filepath.Dir(cleanPath) + filename := filepath.Base(cleanPath) + + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + + root, err := os.OpenRoot(dir) + if err != nil { + return err + } + defer func() { + if cerr := root.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + data := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + + f, err := root.Create(filename) + if err != nil { + return err + } + defer func() { + if cerr := f.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + if err := f.Chmod(0o600); err != nil { + return err + } + + _, err = f.Write(data) + return err +} diff --git a/cmd/serve.go b/cmd/serve.go index b3d64a0..6e71157 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -10,21 +10,65 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/traPtitech/portal-oidc/internal/repository" + "github.com/traPtitech/portal-oidc/internal/repository/oauth" "github.com/traPtitech/portal-oidc/internal/repository/oidc" + "github.com/traPtitech/portal-oidc/internal/repository/portal" + v1 "github.com/traPtitech/portal-oidc/internal/router/v1" "github.com/traPtitech/portal-oidc/internal/router/v1/gen" + "github.com/traPtitech/portal-oidc/internal/usecase" ) -type Handler struct { - queries *oidc.Queries -} - func newServer(cfg Config) (http.Handler, error) { - queries, err := setupDatabase(cfg.Database) + oidcDB, queries, err := setupOIDCDatabase(cfg.Database) if err != nil { return nil, err } - handler := &Handler{queries: queries} + privateKey, err := loadOrGenerateKey(cfg.OAuth.KeyFile) + if err != nil { + return nil, fmt.Errorf("failed to load/generate RSA key: %w", err) + } + + clientRepo := repository.NewClientRepository(queries) + oauthStorage := oauth.NewStorage( + oidcDB, + queries, + clientRepo, + repository.NewAuthCodeRepository(queries), + repository.NewTokenRepository(queries), + repository.NewOIDCSessionRepository(queries), + ) + defaults := defaultOAuthProviderConfig() + oauth2Provider := newOAuthProvider(oauthStorage, OAuthProviderConfig{ + Issuer: cfg.Host, + AccessTokenLifespan: defaults.AccessTokenLifespan, + RefreshTokenLifespan: defaults.RefreshTokenLifespan, + AuthCodeLifespan: defaults.AuthCodeLifespan, + IDTokenLifespan: defaults.IDTokenLifespan, + Secret: []byte(cfg.OAuth.Secret), + }, privateKey) + + var userUseCase usecase.UserUseCase + if cfg.Environment == "production" { + portalQueries, portalErr := setupPortalDatabase(cfg.Portal.Database) + if portalErr != nil { + return nil, portalErr + } + userUseCase = usecase.NewUserUseCase(repository.NewUserRepository(portalQueries)) + } + handler := v1.NewHandler( + usecase.NewClientUseCase(clientRepo), + oauth2Provider, + userUseCase, + v1.OAuthConfig{ + Issuer: cfg.Host, + SessionSecret: []byte(cfg.OAuth.Secret), + PrivateKey: privateKey, + Environment: cfg.Environment, + TestUserID: cfg.OAuth.TestUserID, + }, + ) e := echo.New() e.Use(middleware.Recover()) @@ -33,9 +77,13 @@ func newServer(cfg Config) (http.Handler, error) { AllowHeaders: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, })) - gen.RegisterHandlers(e, handler) - + e.POST("/oauth2/authorize", func(c echo.Context) error { + return handler.Authorize(c, gen.AuthorizeParams{}) + }) + e.GET("/login", handler.GetLogin) + e.POST("/login", handler.PostLogin) + e.GET("/logout", handler.Logout) e.GET("/health", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) }) @@ -43,22 +91,43 @@ func newServer(cfg Config) (http.Handler, error) { return e, nil } -func setupDatabase(cfg DatabaseConfig) (*oidc.Queries, error) { +func setupOIDCDatabase(cfg DatabaseConfig) (*sql.DB, *oidc.Queries, error) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name) db, err := sql.Open("mysql", dsn) if err != nil { - return nil, fmt.Errorf("failed to open database: %w", err) + return nil, nil, fmt.Errorf("failed to open oidc database: %w", err) } if err := db.PingContext(context.Background()); err != nil { - return nil, fmt.Errorf("failed to ping database: %w", err) + return nil, nil, fmt.Errorf("failed to ping oidc database: %w", err) } queries, err := oidc.Prepare(context.Background(), db) if err != nil { - return nil, fmt.Errorf("failed to prepare queries: %w", err) + return nil, nil, fmt.Errorf("failed to prepare oidc queries: %w", err) + } + + return db, queries, nil +} + +func setupPortalDatabase(cfg DatabaseConfig) (*portal.Queries, error) { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", + cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name) + + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, fmt.Errorf("failed to open portal database: %w", err) + } + + if err := db.PingContext(context.Background()); err != nil { + return nil, fmt.Errorf("failed to ping portal database: %w", err) + } + + queries, err := portal.Prepare(context.Background(), db) + if err != nil { + return nil, fmt.Errorf("failed to prepare portal queries: %w", err) } return queries, nil diff --git a/compose.yaml b/compose.yaml index 220bc52..57f59c4 100644 --- a/compose.yaml +++ b/compose.yaml @@ -13,6 +13,9 @@ services: environment: - CONFIG_FILE=/app/config.yaml - TZ=Asia/Tokyo + - OIDC_DATABASE__HOST=oidc + - OIDC_DATABASE__PORT=3306 + - OIDC_PORTAL__DATABASE__HOST=portal depends_on: portal: condition: service_healthy diff --git a/config.yaml b/config.yaml index 9a3da1a..c304362 100644 --- a/config.yaml +++ b/config.yaml @@ -7,3 +7,14 @@ database: user: root password: password name: oidc + +portal: + database: + host: localhost + port: 3306 + user: root + password: password + name: portal + +oauth: + test_user_id: testuser diff --git a/db/query/oidc.sql b/db/query/oidc.sql index 260c637..bc683e6 100644 --- a/db/query/oidc.sql +++ b/db/query/oidc.sql @@ -28,4 +28,109 @@ UPDATE clients SET WHERE client_id = ?; -- name: DeleteClient :exec -DELETE FROM clients WHERE client_id = ?; \ No newline at end of file +DELETE FROM clients WHERE client_id = ?; + +-- name: DeleteAllClients :exec +DELETE FROM clients; + +-- Authorization Code queries + +-- name: CreateAuthorizationCode :exec +INSERT INTO authorization_codes ( + code, + client_id, + user_id, + redirect_uri, + scopes, + code_challenge, + code_challenge_method, + nonce, + expires_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + +-- name: GetAuthorizationCode :one +SELECT * FROM authorization_codes WHERE code = ?; + +-- name: DeleteAuthorizationCode :exec +DELETE FROM authorization_codes WHERE code = ?; + +-- name: MarkAuthorizationCodeUsed :exec +UPDATE authorization_codes SET used = TRUE WHERE code = ?; + +-- name: UpdateAuthorizationCodePKCE :exec +UPDATE authorization_codes SET + code_challenge = ?, + code_challenge_method = ? +WHERE code = ?; + +-- name: DeleteExpiredAuthorizationCodes :exec +DELETE FROM authorization_codes WHERE expires_at < NOW(); + +-- name: DeleteAllAuthorizationCodes :exec +DELETE FROM authorization_codes; + +-- Token queries + +-- name: CreateToken :exec +INSERT INTO tokens ( + id, + request_id, + client_id, + user_id, + access_token, + refresh_token, + scopes, + expires_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?); + +-- name: GetTokenByAccessToken :one +SELECT * FROM tokens WHERE access_token = ?; + +-- name: GetTokenByRefreshToken :one +SELECT * FROM tokens WHERE refresh_token = ?; + +-- name: GetTokenByID :one +SELECT * FROM tokens WHERE id = ?; + +-- name: DeleteToken :exec +DELETE FROM tokens WHERE id = ?; + +-- name: DeleteTokenByAccessToken :exec +DELETE FROM tokens WHERE access_token = ?; + +-- name: DeleteTokenByRefreshToken :exec +DELETE FROM tokens WHERE refresh_token = ?; + +-- name: DeleteExpiredTokens :exec +DELETE FROM tokens WHERE expires_at < NOW(); + +-- name: DeleteTokensByUserAndClient :exec +DELETE FROM tokens WHERE user_id = ? AND client_id = ?; + +-- name: DeleteTokensByRequestID :exec +DELETE FROM tokens WHERE request_id = ?; + +-- name: DeleteAllTokens :exec +DELETE FROM tokens; + +-- OIDC Session queries + +-- name: CreateOIDCSession :exec +INSERT INTO oidc_sessions ( + authorize_code, + client_id, + user_id, + scopes, + nonce, + auth_time, + requested_at +) VALUES (?, ?, ?, ?, ?, ?, ?); + +-- name: GetOIDCSession :one +SELECT * FROM oidc_sessions WHERE authorize_code = ?; + +-- name: DeleteOIDCSession :exec +DELETE FROM oidc_sessions WHERE authorize_code = ?; + +-- name: DeleteAllOIDCSessions :exec +DELETE FROM oidc_sessions; \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index 2639f4f..0990e42 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,6 +1,6 @@ -- OIDC Schema -CREATE TABLE `clients` ( +CREATE TABLE IF NOT EXISTS `clients` ( `client_id` char(36) NOT NULL, `client_secret_hash` varchar(255) NULL, `name` varchar(255) NOT NULL, @@ -10,3 +10,56 @@ CREATE TABLE `clients` ( `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `authorization_codes` ( + `code` varchar(64) NOT NULL, + `client_id` char(36) NOT NULL, + `user_id` char(36) NOT NULL, + `redirect_uri` text NOT NULL, + `scopes` text NOT NULL, + `code_challenge` varchar(128) NULL, + `code_challenge_method` varchar(10) NULL, + `nonce` varchar(255) NULL, + `used` BOOLEAN NOT NULL DEFAULT FALSE, + `expires_at` datetime(6) NOT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (`code`), + INDEX `idx_authorization_codes_client_id` (`client_id`), + INDEX `idx_authorization_codes_expires_at` (`expires_at`), + CONSTRAINT `fk_authorization_codes_client` FOREIGN KEY (`client_id`) REFERENCES `clients` (`client_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `tokens` ( + `id` char(36) NOT NULL, + `request_id` varchar(64) NOT NULL, + `client_id` char(36) NOT NULL, + `user_id` char(36) NOT NULL, + `access_token` varchar(64) NOT NULL, + `refresh_token` varchar(64) NULL, + `scopes` text NOT NULL, + `expires_at` datetime(6) NOT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`), + UNIQUE INDEX `idx_tokens_access_token` (`access_token`), + UNIQUE INDEX `idx_tokens_refresh_token` (`refresh_token`), + INDEX `idx_tokens_client_id` (`client_id`), + INDEX `idx_tokens_user_id` (`user_id`), + INDEX `idx_tokens_request_id` (`request_id`), + INDEX `idx_tokens_expires_at` (`expires_at`), + CONSTRAINT `fk_tokens_client` FOREIGN KEY (`client_id`) REFERENCES `clients` (`client_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `oidc_sessions` ( + `authorize_code` varchar(255) NOT NULL, + `client_id` char(36) NOT NULL, + `user_id` char(36) NOT NULL, + `scopes` text NOT NULL, + `nonce` varchar(255) NULL, + `auth_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `requested_at` datetime(6) NOT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (`authorize_code`), + INDEX `idx_oidc_sessions_client_id` (`client_id`), + CONSTRAINT `fk_oidc_sessions_client` FOREIGN KEY (`client_id`) + REFERENCES `clients` (`client_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/go.mod b/go.mod index 883a32b..3f82e48 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module github.com/traPtitech/portal-oidc -go 1.25.5 +go 1.25.7 require ( github.com/alecthomas/kong v1.13.0 + github.com/go-jose/go-jose/v4 v4.1.3 github.com/go-sql-driver/mysql v1.9.3 + github.com/google/uuid v1.6.0 + github.com/gorilla/sessions v1.4.0 github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/confmap v1.0.0 github.com/knadh/koanf/providers/env v1.1.0 @@ -12,53 +15,96 @@ require ( github.com/knadh/koanf/v2 v2.3.0 github.com/labstack/echo/v4 v4.15.0 github.com/oapi-codegen/runtime v1.1.2 + github.com/ory/fosite v0.49.0 + golang.org/x/crypto v0.47.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cristalhq/jwt/v4 v4.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/josharian/intern v1.0.0 // indirect + github.com/gobuffalo/pop/v6 v6.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/labstack/gommon v0.4.2 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/goveralls v0.0.12 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect - github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect - github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect - github.com/onsi/gomega v1.34.1 // indirect - github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sergi/go-diff v1.4.0 // indirect - github.com/speakeasy-api/jsonpath v0.6.0 // indirect - github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect + github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe // indirect + github.com/ory/go-convenience v0.1.0 // indirect + github.com/ory/x v0.0.665 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.16.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/subosito/gotenv v1.4.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.21.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.yaml.in/yaml/v3 v3.0.3 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.14.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.40.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen diff --git a/go.sum b/go.sum index 078406c..0ff32c1 100644 --- a/go.sum +++ b/go.sum @@ -1,62 +1,292 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA= github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cristalhq/jwt/v4 v4.0.2 h1:g/AD3h0VicDamtlM70GWGElp8kssQEv+5wYd7L9WOhU= +github.com/cristalhq/jwt/v4 v4.0.2/go.mod h1:HnYraSNKDRag1DZP92rYHyrjyQHnVEHPNqesmzs+miQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= +github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= -github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobuffalo/attrs v1.0.3/go.mod h1:KvDJCE0avbufqS0Bw3UV7RQynESY0jjod+572ctX4t8= +github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= +github.com/gobuffalo/fizz v1.14.4/go.mod h1:9/2fGNXNeIFOXEEgTPJwiK63e44RjG+Nc4hfMm1ArGM= +github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= +github.com/gobuffalo/flect v1.0.0/go.mod h1:l9V6xSb4BlXwsxEMj3FVEub2nkdQjWhPvD8XTTlHPQc= +github.com/gobuffalo/genny/v2 v2.1.0/go.mod h1:4yoTNk4bYuP3BMM6uQKYPvtP6WsXFGm2w2EFYZdRls8= +github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= +github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= +github.com/gobuffalo/logger v1.0.7/go.mod h1:u40u6Bq3VVvaMcy5sRBclD8SXhBYPS0Qk95ubt+1xJM= +github.com/gobuffalo/nulls v0.4.2/go.mod h1:EElw2zmBYafU2R9W4Ii1ByIj177wA/pc0JdjtD0EsH8= +github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= +github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= +github.com/gobuffalo/plush/v4 v4.1.18/go.mod h1:xi2tJIhFI4UdzIL8sxZtzGYOd2xbBpcFbLZlIPGGZhU= +github.com/gobuffalo/pop/v6 v6.1.1 h1:eUDBaZcb0gYrmFnKwpuTEUA7t5ZHqNfvS4POqJYXDZY= +github.com/gobuffalo/pop/v6 v6.1.1/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI= +github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= +github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jandelgado/gcov2lcov v1.0.5 h1:rkBt40h0CVK4oCb8Dps950gvfd1rYvQ8+cWa346lVU0= +github.com/jandelgado/gcov2lcov v1.0.5/go.mod h1:NnSxK6TMlg1oGDBfGelGbjgorT5/L3cchlbtgFYZSss= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= +github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= @@ -65,12 +295,18 @@ github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWy github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= +github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -78,156 +314,597 @@ github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/goveralls v0.0.12 h1:PEEeF0k1SsTjOBQ8FOmrOAoCu4ytuMaWCnWe94zxbCg= +github.com/mattn/goveralls v0.0.12/go.mod h1:44ImGEUfmqH8bBtaMrYKsM65LXfNLWmwaxFGjZwgMSQ= +github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= -github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= +github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= +github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM= +github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60= +github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= +github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/ory/fosite v0.49.0 h1:KNqO7RVt/1X8F08/UI0Y+GRvcpscCWgjqvpLBQPRovo= +github.com/ory/fosite v0.49.0/go.mod h1:FAn7IY+I6DjT1r29wMouPeRYq63DWUuBj++96uOS4mE= +github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= +github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= +github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= +github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs= +github.com/ory/herodot v0.10.2 h1:gGvNMHgAwWzdP/eo+roSiT5CGssygHSjDU7MSQNlJ4E= +github.com/ory/herodot v0.10.2/go.mod h1:MMNmY6MG1uB6fnXYFaHoqdV23DTWctlPsmRCeq/2+wc= +github.com/ory/jsonschema/v3 v3.0.8 h1:Ssdb3eJ4lDZ/+XnGkvQS/te0p+EkolqwTsDOCxr/FmU= +github.com/ory/jsonschema/v3 v3.0.8/go.mod h1:ZPzqjDkwd3QTnb2Z6PAS+OTvBE2x5i6m25wCGx54W/0= +github.com/ory/x v0.0.665 h1:61vv0ObCDSX1vOQYbxBeqDiv4YiPmMT91lYxDaaKX08= +github.com/ory/x v0.0.665/go.mod h1:7SCTki3N0De3ZpqlxhxU/94ZrOCfNEnXwVtd0xVt+L8= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= -github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= -github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= -github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= -github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 h1:0b8DF5kR0PhRoRXDiEEdzrgBc8UqVY4JWLkQJCRsLME= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= +github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= -github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= -github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= -github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/contrib/propagators/b3 v1.21.0 h1:uGdgDPNzwQWRwCXJgw/7h29JaRqcq9B87Iv4hJDKAZw= +go.opentelemetry.io/contrib/propagators/b3 v1.21.0/go.mod h1:D9GQXvVGT2pzyTfp1QBOnD1rzKEWzKjjwu5q2mslCUI= +go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 h1:f4beMGDKiVzg9IcX7/VuWVy+oGdjx3dNJ72YehmtY5k= +go.opentelemetry.io/contrib/propagators/jaeger v1.21.1/go.mod h1:U9jhkEl8d1LL+QXY7q3kneJWJugiN3kZJV2OWz3hkBY= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 h1:Qb+5A+JbIjXwO7l4HkRUhgIn4Bzz0GNS2q+qdmSx+0c= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1/go.mod h1:G4vNCm7fRk0kjZ6pGNLo5SpLxAUvOfSrcaegnT8TPck= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= +go.opentelemetry.io/otel/exporters/zipkin v1.21.0 h1:D+Gv6lSfrFBWmQYyxKjDd0Zuld9SRXpIrEsKZvE4DO4= +go.opentelemetry.io/otel/exporters/zipkin v1.21.0/go.mod h1:83oMKR6DzmHisFOW3I+yIMGZUTjxiWaiBI8M8+TU5zE= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/domain/client.go b/internal/domain/client.go new file mode 100644 index 0000000..1f9581d --- /dev/null +++ b/internal/domain/client.go @@ -0,0 +1,28 @@ +package domain + +import ( + "time" + + "github.com/google/uuid" +) + +type ClientType string + +const ( + ClientTypePublic ClientType = "public" + ClientTypeConfidential ClientType = "confidential" +) + +type Client struct { + ClientID uuid.UUID + Name string + ClientType ClientType + RedirectURIs []string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ClientWithSecret struct { + Client + ClientSecret string // #nosec G117 -- returned only on creation, not persisted +} diff --git a/internal/domain/oauth.go b/internal/domain/oauth.go new file mode 100644 index 0000000..bf95cc1 --- /dev/null +++ b/internal/domain/oauth.go @@ -0,0 +1,40 @@ +package domain + +import "time" + +type AuthCode struct { + Code string + ClientID string + UserID string + RedirectURI string + Scopes []string + CodeChallenge string + CodeChallengeMethod string + Nonce string + Used bool + ExpiresAt time.Time + CreatedAt time.Time +} + +type Token struct { + ID string + RequestID string + ClientID string + UserID string + AccessToken string // #nosec G117 -- domain field name, not a credential + RefreshToken string // #nosec G117 -- domain field name, not a credential + Scopes []string + ExpiresAt time.Time + CreatedAt time.Time +} + +type OIDCSession struct { + AuthorizeCode string + ClientID string + UserID string + Nonce string + AuthTime time.Time + Scopes []string + RequestedAt time.Time + CreatedAt time.Time +} diff --git a/internal/domain/user.go b/internal/domain/user.go new file mode 100644 index 0000000..028a05f --- /dev/null +++ b/internal/domain/user.go @@ -0,0 +1,11 @@ +package domain + +type User struct { + ID string + TrapID string +} + +type UserWithPassword struct { + User + PasswordHash string +} diff --git a/internal/repository/authcode.go b/internal/repository/authcode.go new file mode 100644 index 0000000..d6b41a0 --- /dev/null +++ b/internal/repository/authcode.go @@ -0,0 +1,111 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "strings" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository/oidc" +) + +var ErrAuthCodeNotFound = errors.New("authorization code not found") + +type AuthCodeRepository interface { + Create(ctx context.Context, authCode domain.AuthCode) error + Get(ctx context.Context, code string) (domain.AuthCode, error) + Delete(ctx context.Context, code string) error + MarkUsed(ctx context.Context, code string) error + UpdatePKCE(ctx context.Context, code, challenge, method string) error +} + +type authCodeRepository struct { + queries *oidc.Queries +} + +func NewAuthCodeRepository(queries *oidc.Queries) AuthCodeRepository { + return &authCodeRepository{queries: queries} +} + +func (r *authCodeRepository) Create(ctx context.Context, authCode domain.AuthCode) error { + return r.queries.CreateAuthorizationCode(ctx, oidc.CreateAuthorizationCodeParams{ + Code: authCode.Code, + ClientID: authCode.ClientID, + UserID: authCode.UserID, + RedirectUri: authCode.RedirectURI, + Scopes: strings.Join(authCode.Scopes, " "), + CodeChallenge: sql.NullString{ + String: authCode.CodeChallenge, + Valid: authCode.CodeChallenge != "", + }, + CodeChallengeMethod: sql.NullString{ + String: authCode.CodeChallengeMethod, + Valid: authCode.CodeChallengeMethod != "", + }, + Nonce: sql.NullString{ + String: authCode.Nonce, + Valid: authCode.Nonce != "", + }, + ExpiresAt: authCode.ExpiresAt, + }) +} + +func (r *authCodeRepository) Get(ctx context.Context, code string) (domain.AuthCode, error) { + dbCode, err := r.queries.GetAuthorizationCode(ctx, code) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.AuthCode{}, ErrAuthCodeNotFound + } + return domain.AuthCode{}, err + } + + return toDomainAuthCode(dbCode), nil +} + +func (r *authCodeRepository) Delete(ctx context.Context, code string) error { + return r.queries.DeleteAuthorizationCode(ctx, code) +} + +func (r *authCodeRepository) MarkUsed(ctx context.Context, code string) error { + return r.queries.MarkAuthorizationCodeUsed(ctx, code) +} + +func (r *authCodeRepository) UpdatePKCE(ctx context.Context, code, challenge, method string) error { + return r.queries.UpdateAuthorizationCodePKCE(ctx, oidc.UpdateAuthorizationCodePKCEParams{ + CodeChallenge: sql.NullString{ + String: challenge, + Valid: challenge != "", + }, + CodeChallengeMethod: sql.NullString{ + String: method, + Valid: method != "", + }, + Code: code, + }) +} + +func toDomainAuthCode(db oidc.AuthorizationCode) domain.AuthCode { + scopes := splitScopes(db.Scopes) + + return domain.AuthCode{ + Code: db.Code, + ClientID: db.ClientID, + UserID: db.UserID, + RedirectURI: db.RedirectUri, + Scopes: scopes, + CodeChallenge: db.CodeChallenge.String, + CodeChallengeMethod: db.CodeChallengeMethod.String, + Nonce: db.Nonce.String, + Used: db.Used, + ExpiresAt: db.ExpiresAt, + CreatedAt: db.CreatedAt, + } +} + +func splitScopes(s string) []string { + if s == "" { + return []string{} + } + return strings.Split(s, " ") +} diff --git a/internal/repository/client.go b/internal/repository/client.go new file mode 100644 index 0000000..e0f4cec --- /dev/null +++ b/internal/repository/client.go @@ -0,0 +1,147 @@ +package repository + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + + "github.com/google/uuid" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository/oidc" +) + +var ErrClientNotFound = errors.New("client not found") + +type ClientRepository interface { + Create(ctx context.Context, client *domain.Client, secretHash string) error + Get(ctx context.Context, clientID uuid.UUID) (*domain.Client, error) + GetWithSecretHash(ctx context.Context, clientID uuid.UUID) (*domain.Client, string, error) + List(ctx context.Context) ([]*domain.Client, error) + Update(ctx context.Context, client *domain.Client) error + UpdateSecret(ctx context.Context, clientID uuid.UUID, secretHash string) error + Delete(ctx context.Context, clientID uuid.UUID) error +} + +type clientRepository struct { + queries *oidc.Queries +} + +func NewClientRepository(queries *oidc.Queries) ClientRepository { + return &clientRepository{queries: queries} +} + +func (r *clientRepository) Create(ctx context.Context, client *domain.Client, secretHash string) error { + redirectURIsJSON, err := json.Marshal(client.RedirectURIs) + if err != nil { + return err + } + + return r.queries.CreateClient(ctx, oidc.CreateClientParams{ + ClientID: client.ClientID.String(), + ClientSecretHash: sql.NullString{ + String: secretHash, + Valid: secretHash != "", + }, + Name: client.Name, + ClientType: string(client.ClientType), + RedirectUris: redirectURIsJSON, + }) +} + +func (r *clientRepository) Get(ctx context.Context, clientID uuid.UUID) (*domain.Client, error) { + dbClient, err := r.queries.GetClient(ctx, clientID.String()) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrClientNotFound + } + return nil, err + } + + return r.toDomain(dbClient) +} + +func (r *clientRepository) GetWithSecretHash(ctx context.Context, clientID uuid.UUID) (*domain.Client, string, error) { + dbClient, err := r.queries.GetClient(ctx, clientID.String()) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, "", ErrClientNotFound + } + return nil, "", err + } + + client, err := r.toDomain(dbClient) + if err != nil { + return nil, "", err + } + + return client, dbClient.ClientSecretHash.String, nil +} + +func (r *clientRepository) List(ctx context.Context) ([]*domain.Client, error) { + dbClients, err := r.queries.ListClients(ctx) + if err != nil { + return nil, err + } + + clients := make([]*domain.Client, 0, len(dbClients)) + for _, dbClient := range dbClients { + client, err := r.toDomain(dbClient) + if err != nil { + return nil, err + } + clients = append(clients, client) + } + + return clients, nil +} + +func (r *clientRepository) Update(ctx context.Context, client *domain.Client) error { + redirectURIsJSON, err := json.Marshal(client.RedirectURIs) + if err != nil { + return err + } + + return r.queries.UpdateClient(ctx, oidc.UpdateClientParams{ + ClientID: client.ClientID.String(), + Name: client.Name, + ClientType: string(client.ClientType), + RedirectUris: redirectURIsJSON, + }) +} + +func (r *clientRepository) UpdateSecret(ctx context.Context, clientID uuid.UUID, secretHash string) error { + return r.queries.UpdateClientSecret(ctx, oidc.UpdateClientSecretParams{ + ClientID: clientID.String(), + ClientSecretHash: sql.NullString{ + String: secretHash, + Valid: secretHash != "", + }, + }) +} + +func (r *clientRepository) Delete(ctx context.Context, clientID uuid.UUID) error { + return r.queries.DeleteClient(ctx, clientID.String()) +} + +func (r *clientRepository) toDomain(dbClient oidc.Client) (*domain.Client, error) { + clientID, err := uuid.Parse(dbClient.ClientID) + if err != nil { + return nil, err + } + + var redirectURIs []string + if err := json.Unmarshal(dbClient.RedirectUris, &redirectURIs); err != nil { + return nil, err + } + + return &domain.Client{ + ClientID: clientID, + Name: dbClient.Name, + ClientType: domain.ClientType(dbClient.ClientType), + RedirectURIs: redirectURIs, + CreatedAt: dbClient.CreatedAt, + UpdatedAt: dbClient.UpdatedAt, + }, nil +} diff --git a/internal/repository/oauth/authcode.go b/internal/repository/oauth/authcode.go new file mode 100644 index 0000000..d3bb171 --- /dev/null +++ b/internal/repository/oauth/authcode.go @@ -0,0 +1,95 @@ +package oauth + +import ( + "context" + "errors" + "net/url" + "time" + + "github.com/ory/fosite" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository" +) + +func (s *Storage) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) error { + sess, ok := request.GetSession().(*Session) + if !ok { + return errors.New("invalid session type") + } + + return s.getAuthCodes(ctx).Create(ctx, domain.AuthCode{ + Code: code, + ClientID: request.GetClient().GetID(), + UserID: sess.GetSubject(), + RedirectURI: request.GetRequestForm().Get("redirect_uri"), + Scopes: request.GetRequestedScopes(), + Nonce: request.GetRequestForm().Get("nonce"), + ExpiresAt: sess.GetExpiresAt(fosite.AuthorizeCode), + }) +} + +func (s *Storage) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) { + authCode, err := s.getAuthCodes(ctx).Get(ctx, code) + if err != nil { + if errors.Is(err, repository.ErrAuthCodeNotFound) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + if time.Now().After(authCode.ExpiresAt) { + return nil, fosite.ErrTokenExpired + } + + client, err := s.GetClient(ctx, authCode.ClientID) + if err != nil { + return nil, err + } + + sess := NewSession(authCode.UserID, time.Time{}) + sess.SetExpiresAt(fosite.AuthorizeCode, authCode.ExpiresAt) + + form := url.Values{} + form.Set("redirect_uri", authCode.RedirectURI) + if authCode.CodeChallenge != "" { + form.Set("code_challenge", authCode.CodeChallenge) + } + if authCode.CodeChallengeMethod != "" { + form.Set("code_challenge_method", authCode.CodeChallengeMethod) + } + if authCode.Nonce != "" { + form.Set("nonce", authCode.Nonce) + } + + req := newFositeRequest(code, authCode.CreatedAt, client, sess, authCode.Scopes, form) + + if authCode.Used { + return req, fosite.ErrInvalidatedAuthorizeCode + } + + return req, nil +} + +func (s *Storage) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error { + return s.getAuthCodes(ctx).MarkUsed(ctx, code) +} + +func (s *Storage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + return s.GetAuthorizeCodeSession(ctx, signature, session) +} + +func (s *Storage) CreatePKCERequestSession(ctx context.Context, signature string, requester fosite.Requester) error { + challenge := requester.GetRequestForm().Get("code_challenge") + method := requester.GetRequestForm().Get("code_challenge_method") + + if challenge == "" { + return nil + } + + return s.getAuthCodes(ctx).UpdatePKCE(ctx, signature, challenge, method) +} + +func (s *Storage) DeletePKCERequestSession(ctx context.Context, signature string) error { + return nil +} diff --git a/internal/repository/oauth/client.go b/internal/repository/oauth/client.go new file mode 100644 index 0000000..a3bf9b4 --- /dev/null +++ b/internal/repository/oauth/client.go @@ -0,0 +1,36 @@ +package oauth + +import ( + "github.com/ory/fosite" + "golang.org/x/crypto/bcrypt" +) + +var _ fosite.Client = (*Client)(nil) + +type Client struct { + ID string + Secret []byte // #nosec G117 -- hashed secret, not plaintext + RedirectURIs []string + GrantTypes []string + ResponseTypes []string + Scopes []string + Public bool +} + +func (c *Client) GetID() string { return c.ID } +func (c *Client) GetHashedSecret() []byte { return c.Secret } +func (c *Client) GetRedirectURIs() []string { return c.RedirectURIs } +func (c *Client) GetGrantTypes() fosite.Arguments { return c.GrantTypes } +func (c *Client) GetResponseTypes() fosite.Arguments { + if len(c.ResponseTypes) == 0 { + return []string{"code"} + } + return c.ResponseTypes +} +func (c *Client) GetScopes() fosite.Arguments { return c.Scopes } +func (c *Client) IsPublic() bool { return c.Public } +func (c *Client) GetAudience() fosite.Arguments { return nil } + +func ValidateClientSecret(hashedSecret []byte, secret string) bool { + return bcrypt.CompareHashAndPassword(hashedSecret, []byte(secret)) == nil +} diff --git a/internal/repository/oauth/oidc.go b/internal/repository/oauth/oidc.go new file mode 100644 index 0000000..0174d91 --- /dev/null +++ b/internal/repository/oauth/oidc.go @@ -0,0 +1,57 @@ +package oauth + +import ( + "context" + "errors" + "net/url" + + "github.com/ory/fosite" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository" +) + +func (s *Storage) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) error { + sess, ok := requester.GetSession().(*Session) + if !ok { + return errors.New("invalid session type") + } + + return s.getOIDCSessions(ctx).Create(ctx, domain.OIDCSession{ + AuthorizeCode: authorizeCode, + ClientID: requester.GetClient().GetID(), + UserID: sess.GetSubject(), + Nonce: requester.GetRequestForm().Get("nonce"), + AuthTime: sess.IDTokenClaims().AuthTime, + Scopes: requester.GetGrantedScopes(), + RequestedAt: requester.GetRequestedAt(), + }) +} + +func (s *Storage) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, _ fosite.Requester) (fosite.Requester, error) { + oidcSession, err := s.getOIDCSessions(ctx).Get(ctx, authorizeCode) + if err != nil { + if errors.Is(err, repository.ErrOIDCSessionNotFound) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + client, err := s.GetClient(ctx, oidcSession.ClientID) + if err != nil { + return nil, err + } + + sess := NewSession(oidcSession.UserID, oidcSession.AuthTime) + + form := url.Values{} + if oidcSession.Nonce != "" { + form.Set("nonce", oidcSession.Nonce) + } + + return newFositeRequest(authorizeCode, oidcSession.RequestedAt, client, sess, oidcSession.Scopes, form), nil +} + +func (s *Storage) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error { + return s.getOIDCSessions(ctx).Delete(ctx, authorizeCode) +} diff --git a/internal/repository/oauth/session.go b/internal/repository/oauth/session.go new file mode 100644 index 0000000..7f1c93c --- /dev/null +++ b/internal/repository/oauth/session.go @@ -0,0 +1,76 @@ +package oauth + +import ( + "maps" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/token/jwt" +) + +var _ openid.Session = (*Session)(nil) + +type Session struct { + subject string + username string + expiresAt map[fosite.TokenType]time.Time + extra map[string]interface{} + idTokenClaims *jwt.IDTokenClaims + idTokenHeaders *jwt.Headers +} + +func NewSession(subject string, authTime time.Time) *Session { + return &Session{ + subject: subject, + username: subject, + expiresAt: make(map[fosite.TokenType]time.Time), + extra: make(map[string]interface{}), + idTokenClaims: &jwt.IDTokenClaims{ + Subject: subject, + AuthTime: authTime, + RequestedAt: authTime, + }, + idTokenHeaders: &jwt.Headers{ + Extra: make(map[string]interface{}), + }, + } +} + +func (s *Session) SetExpiresAt(key fosite.TokenType, exp time.Time) { + if s.expiresAt == nil { + s.expiresAt = make(map[fosite.TokenType]time.Time) + } + s.expiresAt[key] = exp +} + +func (s *Session) GetExpiresAt(key fosite.TokenType) time.Time { + if s.expiresAt == nil { + return time.Time{} + } + return s.expiresAt[key] +} + +func (s *Session) GetUsername() string { return s.username } +func (s *Session) GetSubject() string { return s.subject } +func (s *Session) IDTokenClaims() *jwt.IDTokenClaims { return s.idTokenClaims } +func (s *Session) IDTokenHeaders() *jwt.Headers { return s.idTokenHeaders } + +func (s *Session) Clone() fosite.Session { + expiresAt := make(map[fosite.TokenType]time.Time) + maps.Copy(expiresAt, s.expiresAt) + extra := make(map[string]interface{}) + maps.Copy(extra, s.extra) + idTokenClaimsClone := *s.idTokenClaims + idTokenHeadersClone := *s.idTokenHeaders + idTokenHeadersClone.Extra = make(map[string]interface{}) + maps.Copy(idTokenHeadersClone.Extra, s.idTokenHeaders.Extra) + return &Session{ + subject: s.subject, + username: s.username, + expiresAt: expiresAt, + extra: extra, + idTokenClaims: &idTokenClaimsClone, + idTokenHeaders: &idTokenHeadersClone, + } +} diff --git a/internal/repository/oauth/storage.go b/internal/repository/oauth/storage.go new file mode 100644 index 0000000..43dbcdb --- /dev/null +++ b/internal/repository/oauth/storage.go @@ -0,0 +1,165 @@ +package oauth + +import ( + "context" + "database/sql" + "errors" + "net/url" + "time" + + "github.com/google/uuid" + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/pkce" + "github.com/ory/fosite/storage" + + "github.com/traPtitech/portal-oidc/internal/repository" + "github.com/traPtitech/portal-oidc/internal/repository/oidc" +) + +var ( + _ fosite.Storage = (*Storage)(nil) + _ oauth2.CoreStorage = (*Storage)(nil) + _ oauth2.TokenRevocationStorage = (*Storage)(nil) + _ pkce.PKCERequestStorage = (*Storage)(nil) + _ openid.OpenIDConnectRequestStorage = (*Storage)(nil) + _ storage.Transactional = (*Storage)(nil) +) + +type Storage struct { + db *sql.DB + baseQueries *oidc.Queries + clients repository.ClientRepository + authCodes repository.AuthCodeRepository + tokens repository.TokenRepository + oidcSessions repository.OIDCSessionRepository +} + +func NewStorage( + db *sql.DB, + baseQueries *oidc.Queries, + clients repository.ClientRepository, + authCodes repository.AuthCodeRepository, + tokens repository.TokenRepository, + oidcSessions repository.OIDCSessionRepository, +) *Storage { + return &Storage{ + db: db, + baseQueries: baseQueries, + clients: clients, + authCodes: authCodes, + tokens: tokens, + oidcSessions: oidcSessions, + } +} + +type txKey struct{} + +type txState struct { + tx *sql.Tx + authCodes repository.AuthCodeRepository + tokens repository.TokenRepository + oidcSessions repository.OIDCSessionRepository +} + +func (s *Storage) BeginTX(ctx context.Context) (context.Context, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return ctx, err + } + + txQueries := s.baseQueries.WithTx(tx) + + return context.WithValue(ctx, txKey{}, &txState{ + tx: tx, + authCodes: repository.NewAuthCodeRepository(txQueries), + tokens: repository.NewTokenRepository(txQueries), + oidcSessions: repository.NewOIDCSessionRepository(txQueries), + }), nil +} + +func (s *Storage) Commit(ctx context.Context) error { + state, ok := ctx.Value(txKey{}).(*txState) + if !ok { + return errors.New("no transaction in context") + } + return state.tx.Commit() +} + +func (s *Storage) Rollback(ctx context.Context) error { + state, ok := ctx.Value(txKey{}).(*txState) + if !ok { + return errors.New("no transaction in context") + } + return state.tx.Rollback() +} + +func (s *Storage) getAuthCodes(ctx context.Context) repository.AuthCodeRepository { + if state, ok := ctx.Value(txKey{}).(*txState); ok { + return state.authCodes + } + return s.authCodes +} + +func (s *Storage) getTokens(ctx context.Context) repository.TokenRepository { + if state, ok := ctx.Value(txKey{}).(*txState); ok { + return state.tokens + } + return s.tokens +} + +func (s *Storage) getOIDCSessions(ctx context.Context) repository.OIDCSessionRepository { + if state, ok := ctx.Value(txKey{}).(*txState); ok { + return state.oidcSessions + } + return s.oidcSessions +} + +func (s *Storage) GetClient(ctx context.Context, id string) (fosite.Client, error) { + clientID, err := uuid.Parse(id) + if err != nil { + return nil, fosite.ErrNotFound + } + + client, secretHash, err := s.clients.GetWithSecretHash(ctx, clientID) + if err != nil { + if errors.Is(err, repository.ErrClientNotFound) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + return &Client{ + ID: client.ClientID.String(), + Secret: []byte(secretHash), + RedirectURIs: client.RedirectURIs, + GrantTypes: []string{"authorization_code", "refresh_token"}, + ResponseTypes: []string{"code"}, + Scopes: []string{"openid", "profile", "email"}, + Public: client.ClientType == "public", + }, nil +} + +func (s *Storage) ClientAssertionJWTValid(ctx context.Context, jti string) error { + return fosite.ErrNotFound +} + +func (s *Storage) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error { + return nil +} + +func newFositeRequest(id string, requestedAt time.Time, client fosite.Client, session *Session, scopes []string, form url.Values) *fosite.Request { + req := &fosite.Request{ + ID: id, + RequestedAt: requestedAt, + Client: client, + Session: session, + Form: form, + } + req.SetRequestedScopes(scopes) + for _, scope := range scopes { + req.GrantScope(scope) + } + return req +} diff --git a/internal/repository/oauth/token.go b/internal/repository/oauth/token.go new file mode 100644 index 0000000..380b35e --- /dev/null +++ b/internal/repository/oauth/token.go @@ -0,0 +1,111 @@ +package oauth + +import ( + "context" + "errors" + "time" + + "github.com/google/uuid" + "github.com/ory/fosite" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository" +) + +func (s *Storage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error { + sess, ok := request.GetSession().(*Session) + if !ok { + return errors.New("invalid session type") + } + + return s.getTokens(ctx).Create(ctx, domain.Token{ + ID: uuid.New().String(), + RequestID: request.GetID(), + ClientID: request.GetClient().GetID(), + UserID: sess.GetSubject(), + AccessToken: signature, + Scopes: request.GetGrantedScopes(), + ExpiresAt: sess.GetExpiresAt(fosite.AccessToken), + }) +} + +func (s *Storage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + token, err := s.getTokens(ctx).GetByAccessToken(ctx, signature) + if err != nil { + if errors.Is(err, repository.ErrTokenNotFound) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + if time.Now().After(token.ExpiresAt) { + return nil, fosite.ErrTokenExpired + } + + client, err := s.GetClient(ctx, token.ClientID) + if err != nil { + return nil, err + } + + sess := NewSession(token.UserID, time.Time{}) + sess.SetExpiresAt(fosite.AccessToken, token.ExpiresAt) + + return newFositeRequest(token.RequestID, token.CreatedAt, client, sess, token.Scopes, nil), nil +} + +func (s *Storage) DeleteAccessTokenSession(ctx context.Context, signature string) error { + return s.getTokens(ctx).DeleteByAccessToken(ctx, signature) +} + +func (s *Storage) CreateRefreshTokenSession(ctx context.Context, signature string, _ string, request fosite.Requester) error { + sess, ok := request.GetSession().(*Session) + if !ok { + return errors.New("invalid session type") + } + + return s.getTokens(ctx).Create(ctx, domain.Token{ + ID: uuid.New().String(), + RequestID: request.GetID(), + ClientID: request.GetClient().GetID(), + UserID: sess.GetSubject(), + RefreshToken: signature, + Scopes: request.GetGrantedScopes(), + ExpiresAt: sess.GetExpiresAt(fosite.RefreshToken), + }) +} + +func (s *Storage) RotateRefreshToken(ctx context.Context, requestID string, refreshTokenSignature string) error { + return nil +} + +func (s *Storage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + token, err := s.getTokens(ctx).GetByRefreshToken(ctx, signature) + if err != nil { + if errors.Is(err, repository.ErrTokenNotFound) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + client, err := s.GetClient(ctx, token.ClientID) + if err != nil { + return nil, err + } + + sess := NewSession(token.UserID, time.Time{}) + sess.SetExpiresAt(fosite.RefreshToken, token.ExpiresAt) + + return newFositeRequest(token.RequestID, token.CreatedAt, client, sess, token.Scopes, nil), nil +} + +func (s *Storage) DeleteRefreshTokenSession(ctx context.Context, signature string) error { + return s.getTokens(ctx).DeleteByRefreshToken(ctx, signature) +} + +func (s *Storage) RevokeRefreshToken(ctx context.Context, requestID string) error { + return s.getTokens(ctx).DeleteByRequestID(ctx, requestID) +} + +func (s *Storage) RevokeAccessToken(ctx context.Context, requestID string) error { + return s.getTokens(ctx).DeleteByRequestID(ctx, requestID) +} diff --git a/internal/repository/oidc/db.go b/internal/repository/oidc/db.go index e6fea6b..be84468 100644 --- a/internal/repository/oidc/db.go +++ b/internal/repository/oidc/db.go @@ -24,18 +24,87 @@ func New(db DBTX) *Queries { func Prepare(ctx context.Context, db DBTX) (*Queries, error) { q := Queries{db: db} var err error + if q.createAuthorizationCodeStmt, err = db.PrepareContext(ctx, createAuthorizationCode); err != nil { + return nil, fmt.Errorf("error preparing query CreateAuthorizationCode: %w", err) + } if q.createClientStmt, err = db.PrepareContext(ctx, createClient); err != nil { return nil, fmt.Errorf("error preparing query CreateClient: %w", err) } + if q.createOIDCSessionStmt, err = db.PrepareContext(ctx, createOIDCSession); err != nil { + return nil, fmt.Errorf("error preparing query CreateOIDCSession: %w", err) + } + if q.createTokenStmt, err = db.PrepareContext(ctx, createToken); err != nil { + return nil, fmt.Errorf("error preparing query CreateToken: %w", err) + } + if q.deleteAllAuthorizationCodesStmt, err = db.PrepareContext(ctx, deleteAllAuthorizationCodes); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllAuthorizationCodes: %w", err) + } + if q.deleteAllClientsStmt, err = db.PrepareContext(ctx, deleteAllClients); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllClients: %w", err) + } + if q.deleteAllOIDCSessionsStmt, err = db.PrepareContext(ctx, deleteAllOIDCSessions); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllOIDCSessions: %w", err) + } + if q.deleteAllTokensStmt, err = db.PrepareContext(ctx, deleteAllTokens); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllTokens: %w", err) + } + if q.deleteAuthorizationCodeStmt, err = db.PrepareContext(ctx, deleteAuthorizationCode); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAuthorizationCode: %w", err) + } if q.deleteClientStmt, err = db.PrepareContext(ctx, deleteClient); err != nil { return nil, fmt.Errorf("error preparing query DeleteClient: %w", err) } + if q.deleteExpiredAuthorizationCodesStmt, err = db.PrepareContext(ctx, deleteExpiredAuthorizationCodes); err != nil { + return nil, fmt.Errorf("error preparing query DeleteExpiredAuthorizationCodes: %w", err) + } + if q.deleteExpiredTokensStmt, err = db.PrepareContext(ctx, deleteExpiredTokens); err != nil { + return nil, fmt.Errorf("error preparing query DeleteExpiredTokens: %w", err) + } + if q.deleteOIDCSessionStmt, err = db.PrepareContext(ctx, deleteOIDCSession); err != nil { + return nil, fmt.Errorf("error preparing query DeleteOIDCSession: %w", err) + } + if q.deleteTokenStmt, err = db.PrepareContext(ctx, deleteToken); err != nil { + return nil, fmt.Errorf("error preparing query DeleteToken: %w", err) + } + if q.deleteTokenByAccessTokenStmt, err = db.PrepareContext(ctx, deleteTokenByAccessToken); err != nil { + return nil, fmt.Errorf("error preparing query DeleteTokenByAccessToken: %w", err) + } + if q.deleteTokenByRefreshTokenStmt, err = db.PrepareContext(ctx, deleteTokenByRefreshToken); err != nil { + return nil, fmt.Errorf("error preparing query DeleteTokenByRefreshToken: %w", err) + } + if q.deleteTokensByRequestIDStmt, err = db.PrepareContext(ctx, deleteTokensByRequestID); err != nil { + return nil, fmt.Errorf("error preparing query DeleteTokensByRequestID: %w", err) + } + if q.deleteTokensByUserAndClientStmt, err = db.PrepareContext(ctx, deleteTokensByUserAndClient); err != nil { + return nil, fmt.Errorf("error preparing query DeleteTokensByUserAndClient: %w", err) + } + if q.getAuthorizationCodeStmt, err = db.PrepareContext(ctx, getAuthorizationCode); err != nil { + return nil, fmt.Errorf("error preparing query GetAuthorizationCode: %w", err) + } if q.getClientStmt, err = db.PrepareContext(ctx, getClient); err != nil { return nil, fmt.Errorf("error preparing query GetClient: %w", err) } + if q.getOIDCSessionStmt, err = db.PrepareContext(ctx, getOIDCSession); err != nil { + return nil, fmt.Errorf("error preparing query GetOIDCSession: %w", err) + } + if q.getTokenByAccessTokenStmt, err = db.PrepareContext(ctx, getTokenByAccessToken); err != nil { + return nil, fmt.Errorf("error preparing query GetTokenByAccessToken: %w", err) + } + if q.getTokenByIDStmt, err = db.PrepareContext(ctx, getTokenByID); err != nil { + return nil, fmt.Errorf("error preparing query GetTokenByID: %w", err) + } + if q.getTokenByRefreshTokenStmt, err = db.PrepareContext(ctx, getTokenByRefreshToken); err != nil { + return nil, fmt.Errorf("error preparing query GetTokenByRefreshToken: %w", err) + } if q.listClientsStmt, err = db.PrepareContext(ctx, listClients); err != nil { return nil, fmt.Errorf("error preparing query ListClients: %w", err) } + if q.markAuthorizationCodeUsedStmt, err = db.PrepareContext(ctx, markAuthorizationCodeUsed); err != nil { + return nil, fmt.Errorf("error preparing query MarkAuthorizationCodeUsed: %w", err) + } + if q.updateAuthorizationCodePKCEStmt, err = db.PrepareContext(ctx, updateAuthorizationCodePKCE); err != nil { + return nil, fmt.Errorf("error preparing query UpdateAuthorizationCodePKCE: %w", err) + } if q.updateClientStmt, err = db.PrepareContext(ctx, updateClient); err != nil { return nil, fmt.Errorf("error preparing query UpdateClient: %w", err) } @@ -47,26 +116,141 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { func (q *Queries) Close() error { var err error + if q.createAuthorizationCodeStmt != nil { + if cerr := q.createAuthorizationCodeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createAuthorizationCodeStmt: %w", cerr) + } + } if q.createClientStmt != nil { if cerr := q.createClientStmt.Close(); cerr != nil { err = fmt.Errorf("error closing createClientStmt: %w", cerr) } } + if q.createOIDCSessionStmt != nil { + if cerr := q.createOIDCSessionStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createOIDCSessionStmt: %w", cerr) + } + } + if q.createTokenStmt != nil { + if cerr := q.createTokenStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createTokenStmt: %w", cerr) + } + } + if q.deleteAllAuthorizationCodesStmt != nil { + if cerr := q.deleteAllAuthorizationCodesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllAuthorizationCodesStmt: %w", cerr) + } + } + if q.deleteAllClientsStmt != nil { + if cerr := q.deleteAllClientsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllClientsStmt: %w", cerr) + } + } + if q.deleteAllOIDCSessionsStmt != nil { + if cerr := q.deleteAllOIDCSessionsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllOIDCSessionsStmt: %w", cerr) + } + } + if q.deleteAllTokensStmt != nil { + if cerr := q.deleteAllTokensStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllTokensStmt: %w", cerr) + } + } + if q.deleteAuthorizationCodeStmt != nil { + if cerr := q.deleteAuthorizationCodeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAuthorizationCodeStmt: %w", cerr) + } + } if q.deleteClientStmt != nil { if cerr := q.deleteClientStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteClientStmt: %w", cerr) } } + if q.deleteExpiredAuthorizationCodesStmt != nil { + if cerr := q.deleteExpiredAuthorizationCodesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteExpiredAuthorizationCodesStmt: %w", cerr) + } + } + if q.deleteExpiredTokensStmt != nil { + if cerr := q.deleteExpiredTokensStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteExpiredTokensStmt: %w", cerr) + } + } + if q.deleteOIDCSessionStmt != nil { + if cerr := q.deleteOIDCSessionStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteOIDCSessionStmt: %w", cerr) + } + } + if q.deleteTokenStmt != nil { + if cerr := q.deleteTokenStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteTokenStmt: %w", cerr) + } + } + if q.deleteTokenByAccessTokenStmt != nil { + if cerr := q.deleteTokenByAccessTokenStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteTokenByAccessTokenStmt: %w", cerr) + } + } + if q.deleteTokenByRefreshTokenStmt != nil { + if cerr := q.deleteTokenByRefreshTokenStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteTokenByRefreshTokenStmt: %w", cerr) + } + } + if q.deleteTokensByRequestIDStmt != nil { + if cerr := q.deleteTokensByRequestIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteTokensByRequestIDStmt: %w", cerr) + } + } + if q.deleteTokensByUserAndClientStmt != nil { + if cerr := q.deleteTokensByUserAndClientStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteTokensByUserAndClientStmt: %w", cerr) + } + } + if q.getAuthorizationCodeStmt != nil { + if cerr := q.getAuthorizationCodeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getAuthorizationCodeStmt: %w", cerr) + } + } if q.getClientStmt != nil { if cerr := q.getClientStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getClientStmt: %w", cerr) } } + if q.getOIDCSessionStmt != nil { + if cerr := q.getOIDCSessionStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getOIDCSessionStmt: %w", cerr) + } + } + if q.getTokenByAccessTokenStmt != nil { + if cerr := q.getTokenByAccessTokenStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getTokenByAccessTokenStmt: %w", cerr) + } + } + if q.getTokenByIDStmt != nil { + if cerr := q.getTokenByIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getTokenByIDStmt: %w", cerr) + } + } + if q.getTokenByRefreshTokenStmt != nil { + if cerr := q.getTokenByRefreshTokenStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getTokenByRefreshTokenStmt: %w", cerr) + } + } if q.listClientsStmt != nil { if cerr := q.listClientsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listClientsStmt: %w", cerr) } } + if q.markAuthorizationCodeUsedStmt != nil { + if cerr := q.markAuthorizationCodeUsedStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing markAuthorizationCodeUsedStmt: %w", cerr) + } + } + if q.updateAuthorizationCodePKCEStmt != nil { + if cerr := q.updateAuthorizationCodePKCEStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateAuthorizationCodePKCEStmt: %w", cerr) + } + } if q.updateClientStmt != nil { if cerr := q.updateClientStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateClientStmt: %w", cerr) @@ -114,25 +298,71 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar } type Queries struct { - db DBTX - tx *sql.Tx - createClientStmt *sql.Stmt - deleteClientStmt *sql.Stmt - getClientStmt *sql.Stmt - listClientsStmt *sql.Stmt - updateClientStmt *sql.Stmt - updateClientSecretStmt *sql.Stmt + db DBTX + tx *sql.Tx + createAuthorizationCodeStmt *sql.Stmt + createClientStmt *sql.Stmt + createOIDCSessionStmt *sql.Stmt + createTokenStmt *sql.Stmt + deleteAllAuthorizationCodesStmt *sql.Stmt + deleteAllClientsStmt *sql.Stmt + deleteAllOIDCSessionsStmt *sql.Stmt + deleteAllTokensStmt *sql.Stmt + deleteAuthorizationCodeStmt *sql.Stmt + deleteClientStmt *sql.Stmt + deleteExpiredAuthorizationCodesStmt *sql.Stmt + deleteExpiredTokensStmt *sql.Stmt + deleteOIDCSessionStmt *sql.Stmt + deleteTokenStmt *sql.Stmt + deleteTokenByAccessTokenStmt *sql.Stmt + deleteTokenByRefreshTokenStmt *sql.Stmt + deleteTokensByRequestIDStmt *sql.Stmt + deleteTokensByUserAndClientStmt *sql.Stmt + getAuthorizationCodeStmt *sql.Stmt + getClientStmt *sql.Stmt + getOIDCSessionStmt *sql.Stmt + getTokenByAccessTokenStmt *sql.Stmt + getTokenByIDStmt *sql.Stmt + getTokenByRefreshTokenStmt *sql.Stmt + listClientsStmt *sql.Stmt + markAuthorizationCodeUsedStmt *sql.Stmt + updateAuthorizationCodePKCEStmt *sql.Stmt + updateClientStmt *sql.Stmt + updateClientSecretStmt *sql.Stmt } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ - db: tx, - tx: tx, - createClientStmt: q.createClientStmt, - deleteClientStmt: q.deleteClientStmt, - getClientStmt: q.getClientStmt, - listClientsStmt: q.listClientsStmt, - updateClientStmt: q.updateClientStmt, - updateClientSecretStmt: q.updateClientSecretStmt, + db: tx, + tx: tx, + createAuthorizationCodeStmt: q.createAuthorizationCodeStmt, + createClientStmt: q.createClientStmt, + createOIDCSessionStmt: q.createOIDCSessionStmt, + createTokenStmt: q.createTokenStmt, + deleteAllAuthorizationCodesStmt: q.deleteAllAuthorizationCodesStmt, + deleteAllClientsStmt: q.deleteAllClientsStmt, + deleteAllOIDCSessionsStmt: q.deleteAllOIDCSessionsStmt, + deleteAllTokensStmt: q.deleteAllTokensStmt, + deleteAuthorizationCodeStmt: q.deleteAuthorizationCodeStmt, + deleteClientStmt: q.deleteClientStmt, + deleteExpiredAuthorizationCodesStmt: q.deleteExpiredAuthorizationCodesStmt, + deleteExpiredTokensStmt: q.deleteExpiredTokensStmt, + deleteOIDCSessionStmt: q.deleteOIDCSessionStmt, + deleteTokenStmt: q.deleteTokenStmt, + deleteTokenByAccessTokenStmt: q.deleteTokenByAccessTokenStmt, + deleteTokenByRefreshTokenStmt: q.deleteTokenByRefreshTokenStmt, + deleteTokensByRequestIDStmt: q.deleteTokensByRequestIDStmt, + deleteTokensByUserAndClientStmt: q.deleteTokensByUserAndClientStmt, + getAuthorizationCodeStmt: q.getAuthorizationCodeStmt, + getClientStmt: q.getClientStmt, + getOIDCSessionStmt: q.getOIDCSessionStmt, + getTokenByAccessTokenStmt: q.getTokenByAccessTokenStmt, + getTokenByIDStmt: q.getTokenByIDStmt, + getTokenByRefreshTokenStmt: q.getTokenByRefreshTokenStmt, + listClientsStmt: q.listClientsStmt, + markAuthorizationCodeUsedStmt: q.markAuthorizationCodeUsedStmt, + updateAuthorizationCodePKCEStmt: q.updateAuthorizationCodePKCEStmt, + updateClientStmt: q.updateClientStmt, + updateClientSecretStmt: q.updateClientSecretStmt, } } diff --git a/internal/repository/oidc/models.go b/internal/repository/oidc/models.go index 3a0dd8f..e12a274 100644 --- a/internal/repository/oidc/models.go +++ b/internal/repository/oidc/models.go @@ -10,6 +10,20 @@ import ( "time" ) +type AuthorizationCode struct { + Code string `json:"code"` + ClientID string `json:"client_id"` + UserID string `json:"user_id"` + RedirectUri string `json:"redirect_uri"` + Scopes string `json:"scopes"` + CodeChallenge sql.NullString `json:"code_challenge"` + CodeChallengeMethod sql.NullString `json:"code_challenge_method"` + Nonce sql.NullString `json:"nonce"` + Used bool `json:"used"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` +} + type Client struct { ClientID string `json:"client_id"` ClientSecretHash sql.NullString `json:"client_secret_hash"` @@ -19,3 +33,26 @@ type Client struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } + +type OidcSession struct { + AuthorizeCode string `json:"authorize_code"` + ClientID string `json:"client_id"` + UserID string `json:"user_id"` + Scopes string `json:"scopes"` + Nonce sql.NullString `json:"nonce"` + AuthTime time.Time `json:"auth_time"` + RequestedAt time.Time `json:"requested_at"` + CreatedAt time.Time `json:"created_at"` +} + +type Token struct { + ID string `json:"id"` + RequestID string `json:"request_id"` + ClientID string `json:"client_id"` + UserID string `json:"user_id"` + AccessToken string `json:"access_token"` + RefreshToken sql.NullString `json:"refresh_token"` + Scopes string `json:"scopes"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/internal/repository/oidc/oidc.sql.go b/internal/repository/oidc/oidc.sql.go index 31420be..5ced6c4 100644 --- a/internal/repository/oidc/oidc.sql.go +++ b/internal/repository/oidc/oidc.sql.go @@ -9,8 +9,52 @@ import ( "context" "database/sql" "encoding/json" + "time" ) +const createAuthorizationCode = `-- name: CreateAuthorizationCode :exec + +INSERT INTO authorization_codes ( + code, + client_id, + user_id, + redirect_uri, + scopes, + code_challenge, + code_challenge_method, + nonce, + expires_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateAuthorizationCodeParams struct { + Code string `json:"code"` + ClientID string `json:"client_id"` + UserID string `json:"user_id"` + RedirectUri string `json:"redirect_uri"` + Scopes string `json:"scopes"` + CodeChallenge sql.NullString `json:"code_challenge"` + CodeChallengeMethod sql.NullString `json:"code_challenge_method"` + Nonce sql.NullString `json:"nonce"` + ExpiresAt time.Time `json:"expires_at"` +} + +// Authorization Code queries +func (q *Queries) CreateAuthorizationCode(ctx context.Context, arg CreateAuthorizationCodeParams) error { + _, err := q.exec(ctx, q.createAuthorizationCodeStmt, createAuthorizationCode, + arg.Code, + arg.ClientID, + arg.UserID, + arg.RedirectUri, + arg.Scopes, + arg.CodeChallenge, + arg.CodeChallengeMethod, + arg.Nonce, + arg.ExpiresAt, + ) + return err +} + const createClient = `-- name: CreateClient :exec INSERT INTO clients ( @@ -42,6 +86,128 @@ func (q *Queries) CreateClient(ctx context.Context, arg CreateClientParams) erro return err } +const createOIDCSession = `-- name: CreateOIDCSession :exec + +INSERT INTO oidc_sessions ( + authorize_code, + client_id, + user_id, + scopes, + nonce, + auth_time, + requested_at +) VALUES (?, ?, ?, ?, ?, ?, ?) +` + +type CreateOIDCSessionParams struct { + AuthorizeCode string `json:"authorize_code"` + ClientID string `json:"client_id"` + UserID string `json:"user_id"` + Scopes string `json:"scopes"` + Nonce sql.NullString `json:"nonce"` + AuthTime time.Time `json:"auth_time"` + RequestedAt time.Time `json:"requested_at"` +} + +// OIDC Session queries +func (q *Queries) CreateOIDCSession(ctx context.Context, arg CreateOIDCSessionParams) error { + _, err := q.exec(ctx, q.createOIDCSessionStmt, createOIDCSession, + arg.AuthorizeCode, + arg.ClientID, + arg.UserID, + arg.Scopes, + arg.Nonce, + arg.AuthTime, + arg.RequestedAt, + ) + return err +} + +const createToken = `-- name: CreateToken :exec + +INSERT INTO tokens ( + id, + request_id, + client_id, + user_id, + access_token, + refresh_token, + scopes, + expires_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateTokenParams struct { + ID string `json:"id"` + RequestID string `json:"request_id"` + ClientID string `json:"client_id"` + UserID string `json:"user_id"` + AccessToken string `json:"access_token"` + RefreshToken sql.NullString `json:"refresh_token"` + Scopes string `json:"scopes"` + ExpiresAt time.Time `json:"expires_at"` +} + +// Token queries +func (q *Queries) CreateToken(ctx context.Context, arg CreateTokenParams) error { + _, err := q.exec(ctx, q.createTokenStmt, createToken, + arg.ID, + arg.RequestID, + arg.ClientID, + arg.UserID, + arg.AccessToken, + arg.RefreshToken, + arg.Scopes, + arg.ExpiresAt, + ) + return err +} + +const deleteAllAuthorizationCodes = `-- name: DeleteAllAuthorizationCodes :exec +DELETE FROM authorization_codes +` + +func (q *Queries) DeleteAllAuthorizationCodes(ctx context.Context) error { + _, err := q.exec(ctx, q.deleteAllAuthorizationCodesStmt, deleteAllAuthorizationCodes) + return err +} + +const deleteAllClients = `-- name: DeleteAllClients :exec +DELETE FROM clients +` + +func (q *Queries) DeleteAllClients(ctx context.Context) error { + _, err := q.exec(ctx, q.deleteAllClientsStmt, deleteAllClients) + return err +} + +const deleteAllOIDCSessions = `-- name: DeleteAllOIDCSessions :exec +DELETE FROM oidc_sessions +` + +func (q *Queries) DeleteAllOIDCSessions(ctx context.Context) error { + _, err := q.exec(ctx, q.deleteAllOIDCSessionsStmt, deleteAllOIDCSessions) + return err +} + +const deleteAllTokens = `-- name: DeleteAllTokens :exec +DELETE FROM tokens +` + +func (q *Queries) DeleteAllTokens(ctx context.Context) error { + _, err := q.exec(ctx, q.deleteAllTokensStmt, deleteAllTokens) + return err +} + +const deleteAuthorizationCode = `-- name: DeleteAuthorizationCode :exec +DELETE FROM authorization_codes WHERE code = ? +` + +func (q *Queries) DeleteAuthorizationCode(ctx context.Context, code string) error { + _, err := q.exec(ctx, q.deleteAuthorizationCodeStmt, deleteAuthorizationCode, code) + return err +} + const deleteClient = `-- name: DeleteClient :exec DELETE FROM clients WHERE client_id = ? ` @@ -51,6 +217,106 @@ func (q *Queries) DeleteClient(ctx context.Context, clientID string) error { return err } +const deleteExpiredAuthorizationCodes = `-- name: DeleteExpiredAuthorizationCodes :exec +DELETE FROM authorization_codes WHERE expires_at < NOW() +` + +func (q *Queries) DeleteExpiredAuthorizationCodes(ctx context.Context) error { + _, err := q.exec(ctx, q.deleteExpiredAuthorizationCodesStmt, deleteExpiredAuthorizationCodes) + return err +} + +const deleteExpiredTokens = `-- name: DeleteExpiredTokens :exec +DELETE FROM tokens WHERE expires_at < NOW() +` + +func (q *Queries) DeleteExpiredTokens(ctx context.Context) error { + _, err := q.exec(ctx, q.deleteExpiredTokensStmt, deleteExpiredTokens) + return err +} + +const deleteOIDCSession = `-- name: DeleteOIDCSession :exec +DELETE FROM oidc_sessions WHERE authorize_code = ? +` + +func (q *Queries) DeleteOIDCSession(ctx context.Context, authorizeCode string) error { + _, err := q.exec(ctx, q.deleteOIDCSessionStmt, deleteOIDCSession, authorizeCode) + return err +} + +const deleteToken = `-- name: DeleteToken :exec +DELETE FROM tokens WHERE id = ? +` + +func (q *Queries) DeleteToken(ctx context.Context, id string) error { + _, err := q.exec(ctx, q.deleteTokenStmt, deleteToken, id) + return err +} + +const deleteTokenByAccessToken = `-- name: DeleteTokenByAccessToken :exec +DELETE FROM tokens WHERE access_token = ? +` + +func (q *Queries) DeleteTokenByAccessToken(ctx context.Context, accessToken string) error { + _, err := q.exec(ctx, q.deleteTokenByAccessTokenStmt, deleteTokenByAccessToken, accessToken) + return err +} + +const deleteTokenByRefreshToken = `-- name: DeleteTokenByRefreshToken :exec +DELETE FROM tokens WHERE refresh_token = ? +` + +func (q *Queries) DeleteTokenByRefreshToken(ctx context.Context, refreshToken sql.NullString) error { + _, err := q.exec(ctx, q.deleteTokenByRefreshTokenStmt, deleteTokenByRefreshToken, refreshToken) + return err +} + +const deleteTokensByRequestID = `-- name: DeleteTokensByRequestID :exec +DELETE FROM tokens WHERE request_id = ? +` + +func (q *Queries) DeleteTokensByRequestID(ctx context.Context, requestID string) error { + _, err := q.exec(ctx, q.deleteTokensByRequestIDStmt, deleteTokensByRequestID, requestID) + return err +} + +const deleteTokensByUserAndClient = `-- name: DeleteTokensByUserAndClient :exec +DELETE FROM tokens WHERE user_id = ? AND client_id = ? +` + +type DeleteTokensByUserAndClientParams struct { + UserID string `json:"user_id"` + ClientID string `json:"client_id"` +} + +func (q *Queries) DeleteTokensByUserAndClient(ctx context.Context, arg DeleteTokensByUserAndClientParams) error { + _, err := q.exec(ctx, q.deleteTokensByUserAndClientStmt, deleteTokensByUserAndClient, arg.UserID, arg.ClientID) + return err +} + +const getAuthorizationCode = `-- name: GetAuthorizationCode :one +SELECT code, client_id, user_id, redirect_uri, scopes, code_challenge, code_challenge_method, nonce, used, expires_at, created_at FROM authorization_codes WHERE code = ? +` + +func (q *Queries) GetAuthorizationCode(ctx context.Context, code string) (AuthorizationCode, error) { + row := q.queryRow(ctx, q.getAuthorizationCodeStmt, getAuthorizationCode, code) + var i AuthorizationCode + err := row.Scan( + &i.Code, + &i.ClientID, + &i.UserID, + &i.RedirectUri, + &i.Scopes, + &i.CodeChallenge, + &i.CodeChallengeMethod, + &i.Nonce, + &i.Used, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} + const getClient = `-- name: GetClient :one SELECT client_id, client_secret_hash, name, client_type, redirect_uris, created_at, updated_at FROM clients WHERE client_id = ? ` @@ -70,6 +336,89 @@ func (q *Queries) GetClient(ctx context.Context, clientID string) (Client, error return i, err } +const getOIDCSession = `-- name: GetOIDCSession :one +SELECT authorize_code, client_id, user_id, scopes, nonce, auth_time, requested_at, created_at FROM oidc_sessions WHERE authorize_code = ? +` + +func (q *Queries) GetOIDCSession(ctx context.Context, authorizeCode string) (OidcSession, error) { + row := q.queryRow(ctx, q.getOIDCSessionStmt, getOIDCSession, authorizeCode) + var i OidcSession + err := row.Scan( + &i.AuthorizeCode, + &i.ClientID, + &i.UserID, + &i.Scopes, + &i.Nonce, + &i.AuthTime, + &i.RequestedAt, + &i.CreatedAt, + ) + return i, err +} + +const getTokenByAccessToken = `-- name: GetTokenByAccessToken :one +SELECT id, request_id, client_id, user_id, access_token, refresh_token, scopes, expires_at, created_at FROM tokens WHERE access_token = ? +` + +func (q *Queries) GetTokenByAccessToken(ctx context.Context, accessToken string) (Token, error) { + row := q.queryRow(ctx, q.getTokenByAccessTokenStmt, getTokenByAccessToken, accessToken) + var i Token + err := row.Scan( + &i.ID, + &i.RequestID, + &i.ClientID, + &i.UserID, + &i.AccessToken, + &i.RefreshToken, + &i.Scopes, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} + +const getTokenByID = `-- name: GetTokenByID :one +SELECT id, request_id, client_id, user_id, access_token, refresh_token, scopes, expires_at, created_at FROM tokens WHERE id = ? +` + +func (q *Queries) GetTokenByID(ctx context.Context, id string) (Token, error) { + row := q.queryRow(ctx, q.getTokenByIDStmt, getTokenByID, id) + var i Token + err := row.Scan( + &i.ID, + &i.RequestID, + &i.ClientID, + &i.UserID, + &i.AccessToken, + &i.RefreshToken, + &i.Scopes, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} + +const getTokenByRefreshToken = `-- name: GetTokenByRefreshToken :one +SELECT id, request_id, client_id, user_id, access_token, refresh_token, scopes, expires_at, created_at FROM tokens WHERE refresh_token = ? +` + +func (q *Queries) GetTokenByRefreshToken(ctx context.Context, refreshToken sql.NullString) (Token, error) { + row := q.queryRow(ctx, q.getTokenByRefreshTokenStmt, getTokenByRefreshToken, refreshToken) + var i Token + err := row.Scan( + &i.ID, + &i.RequestID, + &i.ClientID, + &i.UserID, + &i.AccessToken, + &i.RefreshToken, + &i.Scopes, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} + const listClients = `-- name: ListClients :many SELECT client_id, client_secret_hash, name, client_type, redirect_uris, created_at, updated_at FROM clients ` @@ -105,6 +454,33 @@ func (q *Queries) ListClients(ctx context.Context) ([]Client, error) { return items, nil } +const markAuthorizationCodeUsed = `-- name: MarkAuthorizationCodeUsed :exec +UPDATE authorization_codes SET used = TRUE WHERE code = ? +` + +func (q *Queries) MarkAuthorizationCodeUsed(ctx context.Context, code string) error { + _, err := q.exec(ctx, q.markAuthorizationCodeUsedStmt, markAuthorizationCodeUsed, code) + return err +} + +const updateAuthorizationCodePKCE = `-- name: UpdateAuthorizationCodePKCE :exec +UPDATE authorization_codes SET + code_challenge = ?, + code_challenge_method = ? +WHERE code = ? +` + +type UpdateAuthorizationCodePKCEParams struct { + CodeChallenge sql.NullString `json:"code_challenge"` + CodeChallengeMethod sql.NullString `json:"code_challenge_method"` + Code string `json:"code"` +} + +func (q *Queries) UpdateAuthorizationCodePKCE(ctx context.Context, arg UpdateAuthorizationCodePKCEParams) error { + _, err := q.exec(ctx, q.updateAuthorizationCodePKCEStmt, updateAuthorizationCodePKCE, arg.CodeChallenge, arg.CodeChallengeMethod, arg.Code) + return err +} + const updateClient = `-- name: UpdateClient :exec UPDATE clients SET name = ?, diff --git a/internal/repository/oidc/querier.go b/internal/repository/oidc/querier.go index 951d3bd..83fb8d7 100644 --- a/internal/repository/oidc/querier.go +++ b/internal/repository/oidc/querier.go @@ -6,14 +6,41 @@ package oidc import ( "context" + "database/sql" ) type Querier interface { + // Authorization Code queries + CreateAuthorizationCode(ctx context.Context, arg CreateAuthorizationCodeParams) error // Client queries CreateClient(ctx context.Context, arg CreateClientParams) error + // OIDC Session queries + CreateOIDCSession(ctx context.Context, arg CreateOIDCSessionParams) error + // Token queries + CreateToken(ctx context.Context, arg CreateTokenParams) error + DeleteAllAuthorizationCodes(ctx context.Context) error + DeleteAllClients(ctx context.Context) error + DeleteAllOIDCSessions(ctx context.Context) error + DeleteAllTokens(ctx context.Context) error + DeleteAuthorizationCode(ctx context.Context, code string) error DeleteClient(ctx context.Context, clientID string) error + DeleteExpiredAuthorizationCodes(ctx context.Context) error + DeleteExpiredTokens(ctx context.Context) error + DeleteOIDCSession(ctx context.Context, authorizeCode string) error + DeleteToken(ctx context.Context, id string) error + DeleteTokenByAccessToken(ctx context.Context, accessToken string) error + DeleteTokenByRefreshToken(ctx context.Context, refreshToken sql.NullString) error + DeleteTokensByRequestID(ctx context.Context, requestID string) error + DeleteTokensByUserAndClient(ctx context.Context, arg DeleteTokensByUserAndClientParams) error + GetAuthorizationCode(ctx context.Context, code string) (AuthorizationCode, error) GetClient(ctx context.Context, clientID string) (Client, error) + GetOIDCSession(ctx context.Context, authorizeCode string) (OidcSession, error) + GetTokenByAccessToken(ctx context.Context, accessToken string) (Token, error) + GetTokenByID(ctx context.Context, id string) (Token, error) + GetTokenByRefreshToken(ctx context.Context, refreshToken sql.NullString) (Token, error) ListClients(ctx context.Context) ([]Client, error) + MarkAuthorizationCodeUsed(ctx context.Context, code string) error + UpdateAuthorizationCodePKCE(ctx context.Context, arg UpdateAuthorizationCodePKCEParams) error UpdateClient(ctx context.Context, arg UpdateClientParams) error UpdateClientSecret(ctx context.Context, arg UpdateClientSecretParams) error } diff --git a/internal/repository/oidcsession.go b/internal/repository/oidcsession.go new file mode 100644 index 0000000..1531a4d --- /dev/null +++ b/internal/repository/oidcsession.go @@ -0,0 +1,71 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "strings" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository/oidc" +) + +var ErrOIDCSessionNotFound = errors.New("OIDC session not found") + +type OIDCSessionRepository interface { + Create(ctx context.Context, session domain.OIDCSession) error + Get(ctx context.Context, authorizeCode string) (domain.OIDCSession, error) + Delete(ctx context.Context, authorizeCode string) error +} + +type oidcSessionRepository struct { + queries *oidc.Queries +} + +func NewOIDCSessionRepository(queries *oidc.Queries) OIDCSessionRepository { + return &oidcSessionRepository{queries: queries} +} + +func (r *oidcSessionRepository) Create(ctx context.Context, session domain.OIDCSession) error { + return r.queries.CreateOIDCSession(ctx, oidc.CreateOIDCSessionParams{ + AuthorizeCode: session.AuthorizeCode, + ClientID: session.ClientID, + UserID: session.UserID, + Scopes: strings.Join(session.Scopes, " "), + Nonce: sql.NullString{ + String: session.Nonce, + Valid: session.Nonce != "", + }, + AuthTime: session.AuthTime, + RequestedAt: session.RequestedAt, + }) +} + +func (r *oidcSessionRepository) Get(ctx context.Context, authorizeCode string) (domain.OIDCSession, error) { + dbSession, err := r.queries.GetOIDCSession(ctx, authorizeCode) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.OIDCSession{}, ErrOIDCSessionNotFound + } + return domain.OIDCSession{}, err + } + + return toDomainOIDCSession(dbSession), nil +} + +func (r *oidcSessionRepository) Delete(ctx context.Context, authorizeCode string) error { + return r.queries.DeleteOIDCSession(ctx, authorizeCode) +} + +func toDomainOIDCSession(db oidc.OidcSession) domain.OIDCSession { + return domain.OIDCSession{ + AuthorizeCode: db.AuthorizeCode, + ClientID: db.ClientID, + UserID: db.UserID, + Nonce: db.Nonce.String, + AuthTime: db.AuthTime, + Scopes: splitScopes(db.Scopes), + RequestedAt: db.RequestedAt, + CreatedAt: db.CreatedAt, + } +} diff --git a/internal/repository/token.go b/internal/repository/token.go new file mode 100644 index 0000000..0879d72 --- /dev/null +++ b/internal/repository/token.go @@ -0,0 +1,120 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "strings" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository/oidc" +) + +var ErrTokenNotFound = errors.New("token not found") + +type TokenRepository interface { + Create(ctx context.Context, token domain.Token) error + GetByAccessToken(ctx context.Context, accessToken string) (domain.Token, error) + GetByRefreshToken(ctx context.Context, refreshToken string) (domain.Token, error) + GetByID(ctx context.Context, id string) (domain.Token, error) + DeleteByAccessToken(ctx context.Context, accessToken string) error + DeleteByRefreshToken(ctx context.Context, refreshToken string) error + DeleteByID(ctx context.Context, id string) error + DeleteByRequestID(ctx context.Context, requestID string) error +} + +type tokenRepository struct { + queries *oidc.Queries +} + +func NewTokenRepository(queries *oidc.Queries) TokenRepository { + return &tokenRepository{queries: queries} +} + +func (r *tokenRepository) Create(ctx context.Context, token domain.Token) error { + return r.queries.CreateToken(ctx, oidc.CreateTokenParams{ + ID: token.ID, + RequestID: token.RequestID, + ClientID: token.ClientID, + UserID: token.UserID, + AccessToken: token.AccessToken, + RefreshToken: sql.NullString{ + String: token.RefreshToken, + Valid: token.RefreshToken != "", + }, + Scopes: strings.Join(token.Scopes, " "), + ExpiresAt: token.ExpiresAt, + }) +} + +func (r *tokenRepository) GetByAccessToken(ctx context.Context, accessToken string) (domain.Token, error) { + dbToken, err := r.queries.GetTokenByAccessToken(ctx, accessToken) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.Token{}, ErrTokenNotFound + } + return domain.Token{}, err + } + + return toDomainToken(dbToken), nil +} + +func (r *tokenRepository) GetByRefreshToken(ctx context.Context, refreshToken string) (domain.Token, error) { + dbToken, err := r.queries.GetTokenByRefreshToken(ctx, sql.NullString{ + String: refreshToken, + Valid: true, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.Token{}, ErrTokenNotFound + } + return domain.Token{}, err + } + + return toDomainToken(dbToken), nil +} + +func (r *tokenRepository) GetByID(ctx context.Context, id string) (domain.Token, error) { + dbToken, err := r.queries.GetTokenByID(ctx, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.Token{}, ErrTokenNotFound + } + return domain.Token{}, err + } + + return toDomainToken(dbToken), nil +} + +func (r *tokenRepository) DeleteByAccessToken(ctx context.Context, accessToken string) error { + return r.queries.DeleteTokenByAccessToken(ctx, accessToken) +} + +func (r *tokenRepository) DeleteByRefreshToken(ctx context.Context, refreshToken string) error { + return r.queries.DeleteTokenByRefreshToken(ctx, sql.NullString{ + String: refreshToken, + Valid: true, + }) +} + +func (r *tokenRepository) DeleteByID(ctx context.Context, id string) error { + return r.queries.DeleteToken(ctx, id) +} + +func (r *tokenRepository) DeleteByRequestID(ctx context.Context, requestID string) error { + return r.queries.DeleteTokensByRequestID(ctx, requestID) +} + +func toDomainToken(db oidc.Token) domain.Token { + return domain.Token{ + ID: db.ID, + RequestID: db.RequestID, + ClientID: db.ClientID, + UserID: db.UserID, + AccessToken: db.AccessToken, + RefreshToken: db.RefreshToken.String, + Scopes: splitScopes(db.Scopes), + ExpiresAt: db.ExpiresAt, + CreatedAt: db.CreatedAt, + } +} diff --git a/internal/repository/user.go b/internal/repository/user.go new file mode 100644 index 0000000..96add14 --- /dev/null +++ b/internal/repository/user.go @@ -0,0 +1,72 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository/portal" +) + +var ErrUserNotFound = errors.New("user not found") + +type UserRepository interface { + GetByID(ctx context.Context, id string) (*domain.User, error) + GetByTrapID(ctx context.Context, trapID string) (*domain.UserWithPassword, error) + ListStatuses(ctx context.Context, userID string) ([]string, error) +} + +type userRepository struct { + queries *portal.Queries +} + +func NewUserRepository(queries *portal.Queries) UserRepository { + return &userRepository{queries: queries} +} + +func (r *userRepository) GetByID(ctx context.Context, id string) (*domain.User, error) { + user, err := r.queries.GetUserByID(ctx, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } + return nil, err + } + + return &domain.User{ + ID: user.ID, + TrapID: user.TrapID, + }, nil +} + +func (r *userRepository) GetByTrapID(ctx context.Context, trapID string) (*domain.UserWithPassword, error) { + user, err := r.queries.GetUserByTrapID(ctx, trapID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } + return nil, err + } + + return &domain.UserWithPassword{ + User: domain.User{ + ID: user.ID, + TrapID: user.TrapID, + }, + PasswordHash: user.PasswordHash, + }, nil +} + +func (r *userRepository) ListStatuses(ctx context.Context, userID string) ([]string, error) { + statuses, err := r.queries.ListUserStatuses(ctx, userID) + if err != nil { + return nil, err + } + + result := make([]string, len(statuses)) + for i, s := range statuses { + result[i] = s.Status + } + return result, nil +} diff --git a/internal/router/v1/auth.go b/internal/router/v1/auth.go new file mode 100644 index 0000000..3769cf8 --- /dev/null +++ b/internal/router/v1/auth.go @@ -0,0 +1,171 @@ +package v1 + +import ( + "errors" + "html" + "net/http" + "net/url" + "strings" + "time" + + "github.com/labstack/echo/v4" + + "github.com/traPtitech/portal-oidc/internal/usecase" +) + +const sessionName = "oidc_session" + +func (h *Handler) GetLogin(ctx echo.Context) error { + returnURL := sanitizeReturnURL(ctx.QueryParam("return_url")) + + devNote := "" + if h.config.Environment != "production" { + devNote = `

Test user: testuser / password

` + } + + page := ` + + + Login + + + +

Login

+
+ + + + +
+ ` + devNote + ` + +` + + return ctx.HTML(http.StatusOK, page) +} + +func (h *Handler) PostLogin(ctx echo.Context) error { + username := ctx.FormValue("username") + password := ctx.FormValue("password") + returnURL := ctx.FormValue("return_url") + + var userID string + var err error + + if h.config.Environment != "production" { + userID, err = h.authenticateTestUser(username, password) + } else { + userID, err = h.authenticatePortalUser(ctx, username, password) + } + + if err != nil { + return ctx.HTML(http.StatusUnauthorized, ` + +Login Failed + +

Login Failed

+

Invalid username or password.

+ Try again + +`) + } + + session, err := h.sessions.Get(ctx.Request(), sessionName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get session") + } + + session.Values["user_id"] = userID + session.Values["authenticated"] = true + session.Values["auth_time"] = time.Now().Unix() + + if err := session.Save(ctx.Request(), ctx.Response()); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to save session") + } + + return ctx.Redirect(http.StatusFound, sanitizeReturnURL(returnURL)) +} + +func (h *Handler) authenticateTestUser(username, password string) (string, error) { + if username == "testuser" && password == "password" { + return h.config.TestUserID, nil + } + return "", errors.New("invalid credentials") +} + +func (h *Handler) authenticatePortalUser(ctx echo.Context, trapID, password string) (string, error) { + user, err := h.userUseCase.Authenticate(ctx.Request().Context(), trapID, password) + if err != nil { + if errors.Is(err, usecase.ErrUserNotFound) || + errors.Is(err, usecase.ErrInvalidPassword) || + errors.Is(err, usecase.ErrUserNotActive) { + return "", errors.New("authentication failed") + } + return "", err + } + + return user.ID, nil +} + +func (h *Handler) Logout(ctx echo.Context) error { + session, err := h.sessions.Get(ctx.Request(), sessionName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get session") + } + + session.Values["user_id"] = nil + session.Values["authenticated"] = false + session.Options.MaxAge = -1 + + if err := session.Save(ctx.Request(), ctx.Response()); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to save session") + } + + return ctx.Redirect(http.StatusFound, "/") +} + +func sanitizeReturnURL(raw string) string { + if raw == "" { + return "/" + } + parsed, err := url.Parse(raw) + if err != nil || parsed.Host != "" || strings.HasPrefix(raw, "//") { + return "/" + } + return parsed.RequestURI() +} + +type authInfo struct { + UserID string + AuthTime time.Time +} + +func (h *Handler) getAuthInfo(ctx echo.Context) (authInfo, bool) { + session, err := h.sessions.Get(ctx.Request(), sessionName) + if err != nil { + return authInfo{}, false + } + + authenticated, ok := session.Values["authenticated"].(bool) + if !ok || !authenticated { + return authInfo{}, false + } + + userID, ok := session.Values["user_id"].(string) + if !ok { + return authInfo{}, false + } + + at := time.Now() + if authTimeSec, ok := session.Values["auth_time"].(int64); ok { + at = time.Unix(authTimeSec, 0) + } + + return authInfo{UserID: userID, AuthTime: at}, true +} diff --git a/internal/router/v1/client.go b/internal/router/v1/client.go new file mode 100644 index 0000000..644950d --- /dev/null +++ b/internal/router/v1/client.go @@ -0,0 +1,130 @@ +package v1 + +import ( + "errors" + "net/http" + + "github.com/labstack/echo/v4" + openapi_types "github.com/oapi-codegen/runtime/types" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/router/v1/gen" + "github.com/traPtitech/portal-oidc/internal/usecase" +) + +func (h *Handler) GetClients(ctx echo.Context) error { + clients, err := h.clientUseCase.List(ctx.Request().Context()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + response := make([]gen.Client, 0, len(clients)) + for _, c := range clients { + response = append(response, toClientResponse(c)) + } + + return ctx.JSON(http.StatusOK, response) +} + +func (h *Handler) CreateClient(ctx echo.Context) error { + var req gen.ClientCreate + if err := ctx.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + client, err := h.clientUseCase.Create( + ctx.Request().Context(), + req.Name, + domain.ClientType(req.ClientType), + req.RedirectUris, + ) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return ctx.JSON(http.StatusCreated, toClientWithSecretResponse(client)) +} + +func (h *Handler) GetClient(ctx echo.Context, clientId openapi_types.UUID) error { + client, err := h.clientUseCase.Get(ctx.Request().Context(), clientId) + if err != nil { + if errors.Is(err, usecase.ErrClientNotFound) { + return echo.NewHTTPError(http.StatusNotFound, "client not found") + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return ctx.JSON(http.StatusOK, toClientResponse(client)) +} + +func (h *Handler) UpdateClient(ctx echo.Context, clientId openapi_types.UUID) error { + var req gen.ClientUpdate + if err := ctx.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + client, err := h.clientUseCase.Update( + ctx.Request().Context(), + clientId, + req.Name, + domain.ClientType(req.ClientType), + req.RedirectUris, + ) + if err != nil { + if errors.Is(err, usecase.ErrClientNotFound) { + return echo.NewHTTPError(http.StatusNotFound, "client not found") + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return ctx.JSON(http.StatusOK, toClientResponse(client)) +} + +func (h *Handler) DeleteClient(ctx echo.Context, clientId openapi_types.UUID) error { + err := h.clientUseCase.Delete(ctx.Request().Context(), clientId) + if err != nil { + if errors.Is(err, usecase.ErrClientNotFound) { + return echo.NewHTTPError(http.StatusNotFound, "client not found") + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return ctx.NoContent(http.StatusNoContent) +} + +func (h *Handler) RegenerateClientSecret(ctx echo.Context, clientId openapi_types.UUID) error { + secret, err := h.clientUseCase.RegenerateSecret(ctx.Request().Context(), clientId) + if err != nil { + if errors.Is(err, usecase.ErrClientNotFound) { + return echo.NewHTTPError(http.StatusNotFound, "client not found") + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return ctx.JSON(http.StatusOK, gen.ClientSecret{ + ClientSecret: secret, + }) +} + +func toClientResponse(c *domain.Client) gen.Client { + return gen.Client{ + ClientId: c.ClientID, + Name: c.Name, + ClientType: gen.ClientType(c.ClientType), + RedirectUris: c.RedirectURIs, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } +} + +func toClientWithSecretResponse(c *domain.ClientWithSecret) gen.ClientWithSecret { + return gen.ClientWithSecret{ + ClientId: c.ClientID, + Name: c.Name, + ClientType: gen.ClientType(c.ClientType), + RedirectUris: c.RedirectURIs, + ClientSecret: c.ClientSecret, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } +} diff --git a/internal/router/v1/client_test.go b/internal/router/v1/client_test.go new file mode 100644 index 0000000..0ca0d2d --- /dev/null +++ b/internal/router/v1/client_test.go @@ -0,0 +1,491 @@ +package v1 + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + _ "github.com/go-sql-driver/mysql" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/v2" + "github.com/labstack/echo/v4" + "github.com/ory/fosite" + "github.com/ory/fosite/compose" + + "github.com/traPtitech/portal-oidc/internal/repository" + "github.com/traPtitech/portal-oidc/internal/repository/oauth" + "github.com/traPtitech/portal-oidc/internal/repository/oidc" + "github.com/traPtitech/portal-oidc/internal/router/v1/gen" + "github.com/traPtitech/portal-oidc/internal/testutil" + "github.com/traPtitech/portal-oidc/internal/usecase" +) + +const ( + testDBName = "oidc_test" +) + +var testDB *sql.DB + +func TestMain(m *testing.M) { + k := koanf.New(".") + ctx := context.Background() + + _ = k.Load(confmap.Provider(map[string]any{ + "mariadb.username": "root", + "mariadb.password": "password", + "mariadb.hostname": "127.0.0.1", + "mariadb.port": "3307", + }, "."), nil) + + _ = k.Load(env.Provider("MARIADB_", ".", func(s string) string { + return strings.ToLower(strings.TrimPrefix(s, "MARIADB_")) + }), nil) + + user := k.String("mariadb.username") + pass := k.String("mariadb.password") + host := k.String("mariadb.hostname") + port := k.String("mariadb.port") + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/?parseTime=true", user, pass, host, port) + db, err := sql.Open("mysql", dsn) + if err != nil { + fmt.Printf("failed to connect to database: %v\n", err) + os.Exit(1) + } + + if err := db.PingContext(ctx); err != nil { + fmt.Printf("failed to ping database: %v\n", err) + os.Exit(1) + } + + _, err = db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", testDBName)) + if err != nil { + fmt.Printf("failed to create test database: %v\n", err) + os.Exit(1) + } + _ = db.Close() + + dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&multiStatements=true", user, pass, host, port, testDBName) + testDB, err = sql.Open("mysql", dsn) + if err != nil { + fmt.Printf("failed to connect to test database: %v\n", err) + os.Exit(1) + } + + root, err := testutil.FindProjectRoot() + if err != nil { + fmt.Printf("failed to find project root: %v\n", err) + os.Exit(1) + } + schemaPath := filepath.Join(root, "db", "schema.sql") + schemaSQL, err := os.ReadFile(schemaPath) //nolint:gosec + if err != nil { + fmt.Printf("failed to read schema file: %v\n", err) + os.Exit(1) + } + + _, err = testDB.ExecContext(ctx, string(schemaSQL)) + if err != nil { + fmt.Printf("failed to create schema: %v\n", err) + os.Exit(1) + } + + code := m.Run() + + _ = testDB.Close() + + os.Exit(code) +} + +func setupTestHandler(t *testing.T) (*Handler, func()) { + t.Helper() + + ctx := context.Background() + + queries, err := oidc.Prepare(ctx, testDB) + if err != nil { + t.Fatalf("failed to prepare queries: %v", err) + } + + if err := queries.DeleteAllClients(ctx); err != nil { + t.Fatalf("failed to clean up clients table: %v", err) + } + + clientRepo := repository.NewClientRepository(queries) + clientUseCase := usecase.NewClientUseCase(clientRepo) + + oauthStorage := oauth.NewStorage( + testDB, + queries, + clientRepo, + repository.NewAuthCodeRepository(queries), + repository.NewTokenRepository(queries), + repository.NewOIDCSessionRepository(queries), + ) + fositeConfig := &fosite.Config{ //nolint:gosec // test credentials + AccessTokenLifespan: time.Hour, + RefreshTokenLifespan: 30 * 24 * time.Hour, + AuthorizeCodeLifespan: 5 * time.Minute, + IDTokenLifespan: time.Hour, + GlobalSecret: []byte("test-secret-key-32-characters!!"), + ScopeStrategy: fosite.ExactScopeStrategy, + AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy, + SendDebugMessagesToClients: false, + EnforcePKCE: true, + EnforcePKCEForPublicClients: true, + EnablePKCEPlainChallengeMethod: true, + AccessTokenIssuer: "http://localhost:8080", + IDTokenIssuer: "http://localhost:8080", + } + oauth2Provider := compose.Compose( + fositeConfig, + oauthStorage, + compose.NewOAuth2HMACStrategy(fositeConfig), + compose.OAuth2AuthorizeExplicitFactory, + compose.OAuth2PKCEFactory, + compose.OAuth2RefreshTokenGrantFactory, + compose.OAuth2TokenIntrospectionFactory, + compose.OAuth2TokenRevocationFactory, + ) + + handler := NewHandler(clientUseCase, oauth2Provider, nil, OAuthConfig{ + Issuer: "http://localhost:8080", + Environment: "development", + TestUserID: "testuser", + }) + + cleanup := func() { + _ = queries.DeleteAllClients(ctx) + _ = queries.Close() + } + + return handler, cleanup +} + +func TestIntegration_CreateClient(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + reqBody := `{"name":"integration-test-client","client_type":"confidential","redirect_uris":["http://localhost:3000/callback"]}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients", strings.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusCreated { + t.Errorf("status = %d, want %d, body = %s", rec.Code, http.StatusCreated, rec.Body.String()) + } + + var resp gen.ClientWithSecret + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + if resp.Name != "integration-test-client" { + t.Errorf("Name = %q, want %q", resp.Name, "integration-test-client") + } + if resp.ClientType != gen.Confidential { + t.Errorf("ClientType = %q, want %q", resp.ClientType, gen.Confidential) + } + if resp.ClientSecret == "" { + t.Error("ClientSecret should not be empty") + } + if len(resp.RedirectUris) != 1 { + t.Errorf("len(RedirectUris) = %d, want 1", len(resp.RedirectUris)) + } +} + +func TestIntegration_GetClients(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + reqBody := `{"name":"test-client","client_type":"confidential","redirect_uris":["http://localhost:3000/callback"]}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients", strings.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/clients", nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var clients []gen.Client + if err := json.Unmarshal(rec.Body.Bytes(), &clients); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + if len(clients) != 1 { + t.Errorf("len(clients) = %d, want 1", len(clients)) + } +} + +func TestIntegration_GetClient(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + reqBody := `{"name":"test-client","client_type":"confidential","redirect_uris":["http://localhost:3000/callback"]}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients", strings.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + var created gen.ClientWithSecret + if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/clients/"+created.ClientId.String(), nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var client gen.Client + if err := json.Unmarshal(rec.Body.Bytes(), &client); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + if client.ClientId != created.ClientId { + t.Errorf("ClientId = %s, want %s", client.ClientId, created.ClientId) + } + if client.Name != "test-client" { + t.Errorf("Name = %q, want %q", client.Name, "test-client") + } +} + +func TestIntegration_GetClient_NotFound(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/clients/00000000-0000-0000-0000-000000000000", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound) + } +} + +func TestIntegration_UpdateClient(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + reqBody := `{"name":"original","client_type":"confidential","redirect_uris":["http://localhost:3000/callback"]}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients", strings.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + var created gen.ClientWithSecret + if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + updateBody := `{"name":"updated","client_type":"public","redirect_uris":["http://localhost:4000/callback"]}` + req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/clients/"+created.ClientId.String(), strings.NewReader(updateBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d, body = %s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var updated gen.Client + if err := json.Unmarshal(rec.Body.Bytes(), &updated); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + if updated.Name != "updated" { + t.Errorf("Name = %q, want %q", updated.Name, "updated") + } + if updated.ClientType != gen.Public { + t.Errorf("ClientType = %q, want %q", updated.ClientType, gen.Public) + } + if updated.RedirectUris[0] != "http://localhost:4000/callback" { + t.Errorf("RedirectUris[0] = %q, want %q", updated.RedirectUris[0], "http://localhost:4000/callback") + } +} + +func TestIntegration_DeleteClient(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + reqBody := `{"name":"to-delete","client_type":"confidential","redirect_uris":["http://localhost:3000/callback"]}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients", strings.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + var created gen.ClientWithSecret + if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/clients/"+created.ClientId.String(), nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusNoContent { + t.Errorf("status = %d, want %d", rec.Code, http.StatusNoContent) + } + + req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/clients/"+created.ClientId.String(), nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound) + } +} + +func TestIntegration_RegenerateClientSecret(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + reqBody := `{"name":"test-client","client_type":"confidential","redirect_uris":["http://localhost:3000/callback"]}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients", strings.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + var created gen.ClientWithSecret + if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients/"+created.ClientId.String()+"/secret", nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var secret gen.ClientSecret + if err := json.Unmarshal(rec.Body.Bytes(), &secret); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + if secret.ClientSecret == "" { + t.Error("ClientSecret should not be empty") + } + if secret.ClientSecret == created.ClientSecret { + t.Error("new secret should be different from original") + } +} + +func TestIntegration_FullWorkflow(t *testing.T) { + handler, cleanup := setupTestHandler(t) + defer cleanup() + + e := echo.New() + gen.RegisterHandlers(e, handler) + + // 1. Create client + createBody := `{"name":"workflow-test","client_type":"confidential","redirect_uris":["http://localhost:3000/callback"]}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients", strings.NewReader(createBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusCreated { + t.Fatalf("Create: status = %d, want %d", rec.Code, http.StatusCreated) + } + + var created gen.ClientWithSecret + if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + // 2. Verify in list + req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/clients", nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + var clients []gen.Client + if err := json.Unmarshal(rec.Body.Bytes(), &clients); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + if len(clients) != 1 { + t.Errorf("List: len = %d, want 1", len(clients)) + } + + // 3. Update client + updateBody := `{"name":"workflow-updated","client_type":"public","redirect_uris":["http://localhost:4000/callback"]}` + req = httptest.NewRequest(http.MethodPut, "/api/v1/admin/clients/"+created.ClientId.String(), strings.NewReader(updateBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("Update: status = %d, want %d", rec.Code, http.StatusOK) + } + + // 4. Regenerate secret + req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/clients/"+created.ClientId.String()+"/secret", nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("RegenerateSecret: status = %d, want %d", rec.Code, http.StatusOK) + } + + // 5. Delete client + req = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/clients/"+created.ClientId.String(), nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusNoContent { + t.Errorf("Delete: status = %d, want %d", rec.Code, http.StatusNoContent) + } + + // 6. Verify list is empty + req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/clients", nil) + rec = httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if err := json.Unmarshal(rec.Body.Bytes(), &clients); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + if len(clients) != 0 { + t.Errorf("Final List: len = %d, want 0", len(clients)) + } +} diff --git a/internal/router/v1/gen/models.go b/internal/router/v1/gen/models.go index be11371..d0ba90d 100644 --- a/internal/router/v1/gen/models.go +++ b/internal/router/v1/gen/models.go @@ -2,3 +2,266 @@ // // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT. package gen + +import ( + "time" + + openapi_types "github.com/oapi-codegen/runtime/types" +) + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// Defines values for ClientType. +const ( + Confidential ClientType = "confidential" + Public ClientType = "public" +) + +// Defines values for JWKUse. +const ( + Enc JWKUse = "enc" + Sig JWKUse = "sig" +) + +// Defines values for OAuthErrorError. +const ( + AccessDenied OAuthErrorError = "access_denied" + InvalidClient OAuthErrorError = "invalid_client" + InvalidGrant OAuthErrorError = "invalid_grant" + InvalidRequest OAuthErrorError = "invalid_request" + InvalidScope OAuthErrorError = "invalid_scope" + ServerError OAuthErrorError = "server_error" + UnauthorizedClient OAuthErrorError = "unauthorized_client" + UnsupportedGrantType OAuthErrorError = "unsupported_grant_type" +) + +// Defines values for TokenRequestGrantType. +const ( + AuthorizationCode TokenRequestGrantType = "authorization_code" + RefreshToken TokenRequestGrantType = "refresh_token" +) + +// Defines values for TokenResponseTokenType. +const ( + Bearer TokenResponseTokenType = "Bearer" +) + +// Defines values for AuthorizeParamsResponseType. +const ( + Code AuthorizeParamsResponseType = "code" +) + +// Defines values for AuthorizeParamsCodeChallengeMethod. +const ( + Plain AuthorizeParamsCodeChallengeMethod = "plain" + S256 AuthorizeParamsCodeChallengeMethod = "S256" +) + +// Client defines model for Client. +type Client struct { + ClientId openapi_types.UUID `json:"client_id"` + + // ClientType - public: クライアントシークレットを安全に保持できないクライアント (SPA, モバイルアプリ等) <将来実装> + // - confidential: クライアントシークレットを安全に保持できるクライアント (サーバーサイドアプリ) + ClientType ClientType `json:"client_type"` + CreatedAt time.Time `json:"created_at"` + Name string `json:"name"` + RedirectUris []string `json:"redirect_uris"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ClientCreate defines model for ClientCreate. +type ClientCreate struct { + // ClientType - public: クライアントシークレットを安全に保持できないクライアント (SPA, モバイルアプリ等) <将来実装> + // - confidential: クライアントシークレットを安全に保持できるクライアント (サーバーサイドアプリ) + ClientType ClientType `json:"client_type"` + Name string `json:"name"` + RedirectUris []string `json:"redirect_uris"` +} + +// ClientSecret defines model for ClientSecret. +type ClientSecret struct { + // ClientSecret 新しいクライアントシークレット + ClientSecret string `json:"client_secret"` +} + +// ClientType - public: クライアントシークレットを安全に保持できないクライアント (SPA, モバイルアプリ等) <将来実装> +// - confidential: クライアントシークレットを安全に保持できるクライアント (サーバーサイドアプリ) +type ClientType string + +// ClientUpdate defines model for ClientUpdate. +type ClientUpdate struct { + // ClientType - public: クライアントシークレットを安全に保持できないクライアント (SPA, モバイルアプリ等) <将来実装> + // - confidential: クライアントシークレットを安全に保持できるクライアント (サーバーサイドアプリ) + ClientType ClientType `json:"client_type"` + Name string `json:"name"` + RedirectUris []string `json:"redirect_uris"` +} + +// ClientWithSecret defines model for ClientWithSecret. +type ClientWithSecret struct { + ClientId openapi_types.UUID `json:"client_id"` + + // ClientSecret クライアントシークレット (作成時のみ返却、再取得不可) + ClientSecret string `json:"client_secret"` + + // ClientType - public: クライアントシークレットを安全に保持できないクライアント (SPA, モバイルアプリ等) <将来実装> + // - confidential: クライアントシークレットを安全に保持できるクライアント (サーバーサイドアプリ) + ClientType ClientType `json:"client_type"` + CreatedAt time.Time `json:"created_at"` + Name string `json:"name"` + RedirectUris []string `json:"redirect_uris"` + UpdatedAt time.Time `json:"updated_at"` +} + +// JWK defines model for JWK. +type JWK struct { + // Alg Algorithm (RS256 等) + Alg *string `json:"alg,omitempty"` + + // E RSA exponent (Base64url) + E *string `json:"e,omitempty"` + + // Kid Key ID + Kid string `json:"kid"` + + // Kty Key Type (RSA, EC 等) + Kty string `json:"kty"` + + // N RSA modulus (Base64url) + N *string `json:"n,omitempty"` + Use JWKUse `json:"use"` +} + +// JWKUse defines model for JWK.Use. +type JWKUse string + +// JWKS defines model for JWKS. +type JWKS struct { + Keys []JWK `json:"keys"` +} + +// OAuthError defines model for OAuthError. +type OAuthError struct { + Error OAuthErrorError `json:"error"` + + // ErrorDescription エラーの詳細説明 + ErrorDescription *string `json:"error_description,omitempty"` +} + +// OAuthErrorError defines model for OAuthError.Error. +type OAuthErrorError string + +// OpenIDConfiguration defines model for OpenIDConfiguration. +type OpenIDConfiguration struct { + AuthorizationEndpoint string `json:"authorization_endpoint"` + ClaimsSupported *[]string `json:"claims_supported,omitempty"` + CodeChallengeMethodsSupported *[]string `json:"code_challenge_methods_supported,omitempty"` + IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + Issuer string `json:"issuer"` + JwksUri string `json:"jwks_uri"` + ResponseTypesSupported []string `json:"response_types_supported"` + ScopesSupported *[]string `json:"scopes_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported"` + TokenEndpoint string `json:"token_endpoint"` + TokenEndpointAuthMethodsSupported *[]string `json:"token_endpoint_auth_methods_supported,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint"` +} + +// TokenRequest defines model for TokenRequest. +type TokenRequest struct { + ClientId *openapi_types.UUID `json:"client_id,omitempty"` + + // ClientSecret クライアントシークレット (confidential client の場合) + ClientSecret *string `json:"client_secret,omitempty"` + + // Code 認可コード (grant_type=authorization_code 時に必須) + Code *string `json:"code,omitempty"` + + // CodeVerifier PKCE code_verifier + CodeVerifier *string `json:"code_verifier,omitempty"` + GrantType TokenRequestGrantType `json:"grant_type"` + + // RedirectUri リダイレクトURI (grant_type=authorization_code 時に必須) + RedirectUri *string `json:"redirect_uri,omitempty"` + + // RefreshToken リフレッシュトークン (grant_type=refresh_token 時に必須) + RefreshToken *string `json:"refresh_token,omitempty"` +} + +// TokenRequestGrantType defines model for TokenRequest.GrantType. +type TokenRequestGrantType string + +// TokenResponse defines model for TokenResponse. +type TokenResponse struct { + AccessToken string `json:"access_token"` + + // ExpiresIn アクセストークンの有効期限 (秒) + ExpiresIn int `json:"expires_in"` + + // IdToken ID トークン (scope に openid が含まれる場合) + IdToken *string `json:"id_token,omitempty"` + RefreshToken *string `json:"refresh_token,omitempty"` + + // Scope 付与されたスコープ + Scope *string `json:"scope,omitempty"` + TokenType TokenResponseTokenType `json:"token_type"` +} + +// TokenResponseTokenType defines model for TokenResponse.TokenType. +type TokenResponseTokenType string + +// UserInfo defines model for UserInfo. +type UserInfo struct { + Email *openapi_types.Email `json:"email,omitempty"` + EmailVerified *bool `json:"email_verified,omitempty"` + Name *string `json:"name,omitempty"` + Picture *string `json:"picture,omitempty"` + PreferredUsername *string `json:"preferred_username,omitempty"` + + // Sub Subject Identifier + Sub string `json:"sub"` + + // UpdatedAt Unix timestamp + UpdatedAt *int `json:"updated_at,omitempty"` +} + +// AuthorizeParams defines parameters for Authorize. +type AuthorizeParams struct { + // ResponseType レスポンスタイプ (現在は code のみサポート) + ResponseType AuthorizeParamsResponseType `form:"response_type" json:"response_type"` + ClientId openapi_types.UUID `form:"client_id" json:"client_id"` + RedirectUri string `form:"redirect_uri" json:"redirect_uri"` + + // Scope スペース区切りのスコープ (openid, profile, email 等) + Scope *string `form:"scope,omitempty" json:"scope,omitempty"` + + // State CSRF対策用のランダム文字列 + State *string `form:"state,omitempty" json:"state,omitempty"` + + // Nonce リプレイ攻撃対策用 (OIDC) + Nonce *string `form:"nonce,omitempty" json:"nonce,omitempty"` + + // CodeChallenge PKCE code_challenge + CodeChallenge *string `form:"code_challenge,omitempty" json:"code_challenge,omitempty"` + + // CodeChallengeMethod PKCE code_challenge_method + CodeChallengeMethod *AuthorizeParamsCodeChallengeMethod `form:"code_challenge_method,omitempty" json:"code_challenge_method,omitempty"` +} + +// AuthorizeParamsResponseType defines parameters for Authorize. +type AuthorizeParamsResponseType string + +// AuthorizeParamsCodeChallengeMethod defines parameters for Authorize. +type AuthorizeParamsCodeChallengeMethod string + +// CreateClientJSONRequestBody defines body for CreateClient for application/json ContentType. +type CreateClientJSONRequestBody = ClientCreate + +// UpdateClientJSONRequestBody defines body for UpdateClient for application/json ContentType. +type UpdateClientJSONRequestBody = ClientUpdate + +// TokenFormdataRequestBody defines body for Token for application/x-www-form-urlencoded ContentType. +type TokenFormdataRequestBody = TokenRequest diff --git a/internal/router/v1/gen/server.go b/internal/router/v1/gen/server.go index 7712313..4070b95 100644 --- a/internal/router/v1/gen/server.go +++ b/internal/router/v1/gen/server.go @@ -4,12 +4,55 @@ package gen import ( + "context" + "encoding/json" + "fmt" + "net/http" + "github.com/labstack/echo/v4" + "github.com/oapi-codegen/runtime" strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo" + openapi_types "github.com/oapi-codegen/runtime/types" ) // ServerInterface represents all server handlers. type ServerInterface interface { + // JSON Web Key Set + // (GET /.well-known/jwks.json) + GetJWKS(ctx echo.Context) error + // OpenID Provider Configuration + // (GET /.well-known/openid-configuration) + GetOpenIDConfiguration(ctx echo.Context) error + // クライアント一覧取得 + // (GET /api/v1/admin/clients) + GetClients(ctx echo.Context) error + // クライアント作成 + // (POST /api/v1/admin/clients) + CreateClient(ctx echo.Context) error + // クライアント削除 + // (DELETE /api/v1/admin/clients/{clientId}) + DeleteClient(ctx echo.Context, clientId openapi_types.UUID) error + // クライアント取得 + // (GET /api/v1/admin/clients/{clientId}) + GetClient(ctx echo.Context, clientId openapi_types.UUID) error + // クライアント更新 + // (PUT /api/v1/admin/clients/{clientId}) + UpdateClient(ctx echo.Context, clientId openapi_types.UUID) error + // クライアントシークレット再生成 + // (POST /api/v1/admin/clients/{clientId}/secret) + RegenerateClientSecret(ctx echo.Context, clientId openapi_types.UUID) error + // 認可エンドポイント + // (GET /oauth2/authorize) + Authorize(ctx echo.Context, params AuthorizeParams) error + // トークンエンドポイント + // (POST /oauth2/token) + Token(ctx echo.Context) error + // UserInfo エンドポイント (GET) + // (GET /oauth2/userinfo) + GetUserInfo(ctx echo.Context) error + // UserInfo エンドポイント (POST) + // (POST /oauth2/userinfo) + PostUserInfo(ctx echo.Context) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -17,6 +60,204 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } +// GetJWKS converts echo context to params. +func (w *ServerInterfaceWrapper) GetJWKS(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetJWKS(ctx) + return err +} + +// GetOpenIDConfiguration converts echo context to params. +func (w *ServerInterfaceWrapper) GetOpenIDConfiguration(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetOpenIDConfiguration(ctx) + return err +} + +// GetClients converts echo context to params. +func (w *ServerInterfaceWrapper) GetClients(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetClients(ctx) + return err +} + +// CreateClient converts echo context to params. +func (w *ServerInterfaceWrapper) CreateClient(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.CreateClient(ctx) + return err +} + +// DeleteClient converts echo context to params. +func (w *ServerInterfaceWrapper) DeleteClient(ctx echo.Context) error { + var err error + // ------------- Path parameter "clientId" ------------- + var clientId openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "clientId", ctx.Param("clientId"), &clientId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter clientId: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeleteClient(ctx, clientId) + return err +} + +// GetClient converts echo context to params. +func (w *ServerInterfaceWrapper) GetClient(ctx echo.Context) error { + var err error + // ------------- Path parameter "clientId" ------------- + var clientId openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "clientId", ctx.Param("clientId"), &clientId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter clientId: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetClient(ctx, clientId) + return err +} + +// UpdateClient converts echo context to params. +func (w *ServerInterfaceWrapper) UpdateClient(ctx echo.Context) error { + var err error + // ------------- Path parameter "clientId" ------------- + var clientId openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "clientId", ctx.Param("clientId"), &clientId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter clientId: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.UpdateClient(ctx, clientId) + return err +} + +// RegenerateClientSecret converts echo context to params. +func (w *ServerInterfaceWrapper) RegenerateClientSecret(ctx echo.Context) error { + var err error + // ------------- Path parameter "clientId" ------------- + var clientId openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "clientId", ctx.Param("clientId"), &clientId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter clientId: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.RegenerateClientSecret(ctx, clientId) + return err +} + +// Authorize converts echo context to params. +func (w *ServerInterfaceWrapper) Authorize(ctx echo.Context) error { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params AuthorizeParams + // ------------- Required query parameter "response_type" ------------- + + err = runtime.BindQueryParameter("form", true, true, "response_type", ctx.QueryParams(), ¶ms.ResponseType) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter response_type: %s", err)) + } + + // ------------- Required query parameter "client_id" ------------- + + err = runtime.BindQueryParameter("form", true, true, "client_id", ctx.QueryParams(), ¶ms.ClientId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter client_id: %s", err)) + } + + // ------------- Required query parameter "redirect_uri" ------------- + + err = runtime.BindQueryParameter("form", true, true, "redirect_uri", ctx.QueryParams(), ¶ms.RedirectUri) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter redirect_uri: %s", err)) + } + + // ------------- Optional query parameter "scope" ------------- + + err = runtime.BindQueryParameter("form", true, false, "scope", ctx.QueryParams(), ¶ms.Scope) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter scope: %s", err)) + } + + // ------------- Optional query parameter "state" ------------- + + err = runtime.BindQueryParameter("form", true, false, "state", ctx.QueryParams(), ¶ms.State) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter state: %s", err)) + } + + // ------------- Optional query parameter "nonce" ------------- + + err = runtime.BindQueryParameter("form", true, false, "nonce", ctx.QueryParams(), ¶ms.Nonce) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter nonce: %s", err)) + } + + // ------------- Optional query parameter "code_challenge" ------------- + + err = runtime.BindQueryParameter("form", true, false, "code_challenge", ctx.QueryParams(), ¶ms.CodeChallenge) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter code_challenge: %s", err)) + } + + // ------------- Optional query parameter "code_challenge_method" ------------- + + err = runtime.BindQueryParameter("form", true, false, "code_challenge_method", ctx.QueryParams(), ¶ms.CodeChallengeMethod) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter code_challenge_method: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.Authorize(ctx, params) + return err +} + +// Token converts echo context to params. +func (w *ServerInterfaceWrapper) Token(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.Token(ctx) + return err +} + +// GetUserInfo converts echo context to params. +func (w *ServerInterfaceWrapper) GetUserInfo(ctx echo.Context) error { + var err error + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetUserInfo(ctx) + return err +} + +// PostUserInfo converts echo context to params. +func (w *ServerInterfaceWrapper) PostUserInfo(ctx echo.Context) error { + var err error + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PostUserInfo(ctx) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -41,10 +282,400 @@ func RegisterHandlers(router EchoRouter, si ServerInterface) { // can be served under a prefix. func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/.well-known/jwks.json", wrapper.GetJWKS) + router.GET(baseURL+"/.well-known/openid-configuration", wrapper.GetOpenIDConfiguration) + router.GET(baseURL+"/api/v1/admin/clients", wrapper.GetClients) + router.POST(baseURL+"/api/v1/admin/clients", wrapper.CreateClient) + router.DELETE(baseURL+"/api/v1/admin/clients/:clientId", wrapper.DeleteClient) + router.GET(baseURL+"/api/v1/admin/clients/:clientId", wrapper.GetClient) + router.PUT(baseURL+"/api/v1/admin/clients/:clientId", wrapper.UpdateClient) + router.POST(baseURL+"/api/v1/admin/clients/:clientId/secret", wrapper.RegenerateClientSecret) + router.GET(baseURL+"/oauth2/authorize", wrapper.Authorize) + router.POST(baseURL+"/oauth2/token", wrapper.Token) + router.GET(baseURL+"/oauth2/userinfo", wrapper.GetUserInfo) + router.POST(baseURL+"/oauth2/userinfo", wrapper.PostUserInfo) + +} + +type GetJWKSRequestObject struct { +} + +type GetJWKSResponseObject interface { + VisitGetJWKSResponse(w http.ResponseWriter) error +} + +type GetJWKS200JSONResponse JWKS + +func (response GetJWKS200JSONResponse) VisitGetJWKSResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetOpenIDConfigurationRequestObject struct { +} + +type GetOpenIDConfigurationResponseObject interface { + VisitGetOpenIDConfigurationResponse(w http.ResponseWriter) error +} + +type GetOpenIDConfiguration200JSONResponse OpenIDConfiguration + +func (response GetOpenIDConfiguration200JSONResponse) VisitGetOpenIDConfigurationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetClientsRequestObject struct { +} + +type GetClientsResponseObject interface { + VisitGetClientsResponse(w http.ResponseWriter) error +} + +type GetClients200JSONResponse []Client + +func (response GetClients200JSONResponse) VisitGetClientsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetClients401Response struct { +} + +func (response GetClients401Response) VisitGetClientsResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type CreateClientRequestObject struct { + Body *CreateClientJSONRequestBody +} + +type CreateClientResponseObject interface { + VisitCreateClientResponse(w http.ResponseWriter) error +} + +type CreateClient201JSONResponse ClientWithSecret + +func (response CreateClient201JSONResponse) VisitCreateClientResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type CreateClient400Response struct { +} + +func (response CreateClient400Response) VisitCreateClientResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type CreateClient401Response struct { +} + +func (response CreateClient401Response) VisitCreateClientResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type DeleteClientRequestObject struct { + ClientId openapi_types.UUID `json:"clientId"` +} + +type DeleteClientResponseObject interface { + VisitDeleteClientResponse(w http.ResponseWriter) error +} + +type DeleteClient204Response struct { +} + +func (response DeleteClient204Response) VisitDeleteClientResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type DeleteClient401Response struct { +} + +func (response DeleteClient401Response) VisitDeleteClientResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type DeleteClient404Response struct { +} + +func (response DeleteClient404Response) VisitDeleteClientResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type GetClientRequestObject struct { + ClientId openapi_types.UUID `json:"clientId"` +} + +type GetClientResponseObject interface { + VisitGetClientResponse(w http.ResponseWriter) error +} + +type GetClient200JSONResponse Client + +func (response GetClient200JSONResponse) VisitGetClientResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetClient401Response struct { +} + +func (response GetClient401Response) VisitGetClientResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type GetClient404Response struct { +} + +func (response GetClient404Response) VisitGetClientResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type UpdateClientRequestObject struct { + ClientId openapi_types.UUID `json:"clientId"` + Body *UpdateClientJSONRequestBody +} + +type UpdateClientResponseObject interface { + VisitUpdateClientResponse(w http.ResponseWriter) error +} + +type UpdateClient200JSONResponse Client + +func (response UpdateClient200JSONResponse) VisitUpdateClientResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateClient400Response struct { +} + +func (response UpdateClient400Response) VisitUpdateClientResponse(w http.ResponseWriter) error { + w.WriteHeader(400) + return nil +} + +type UpdateClient401Response struct { +} + +func (response UpdateClient401Response) VisitUpdateClientResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type UpdateClient404Response struct { +} + +func (response UpdateClient404Response) VisitUpdateClientResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type RegenerateClientSecretRequestObject struct { + ClientId openapi_types.UUID `json:"clientId"` +} + +type RegenerateClientSecretResponseObject interface { + VisitRegenerateClientSecretResponse(w http.ResponseWriter) error +} + +type RegenerateClientSecret200JSONResponse ClientSecret + +func (response RegenerateClientSecret200JSONResponse) VisitRegenerateClientSecretResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RegenerateClientSecret401Response struct { +} + +func (response RegenerateClientSecret401Response) VisitRegenerateClientSecretResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type RegenerateClientSecret404Response struct { +} + +func (response RegenerateClientSecret404Response) VisitRegenerateClientSecretResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + +type AuthorizeRequestObject struct { + Params AuthorizeParams +} + +type AuthorizeResponseObject interface { + VisitAuthorizeResponse(w http.ResponseWriter) error +} + +type Authorize302Response struct { +} + +func (response Authorize302Response) VisitAuthorizeResponse(w http.ResponseWriter) error { + w.WriteHeader(302) + return nil +} + +type Authorize400JSONResponse OAuthError + +func (response Authorize400JSONResponse) VisitAuthorizeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type TokenRequestObject struct { + Body *TokenFormdataRequestBody +} + +type TokenResponseObject interface { + VisitTokenResponse(w http.ResponseWriter) error +} + +type Token200JSONResponse TokenResponse + +func (response Token200JSONResponse) VisitTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type Token400JSONResponse OAuthError + +func (response Token400JSONResponse) VisitTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type Token401JSONResponse OAuthError + +func (response Token401JSONResponse) VisitTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserInfoRequestObject struct { +} + +type GetUserInfoResponseObject interface { + VisitGetUserInfoResponse(w http.ResponseWriter) error +} + +type GetUserInfo200JSONResponse UserInfo + +func (response GetUserInfo200JSONResponse) VisitGetUserInfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserInfo401Response struct { +} + +func (response GetUserInfo401Response) VisitGetUserInfoResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + +type PostUserInfoRequestObject struct { +} + +type PostUserInfoResponseObject interface { + VisitPostUserInfoResponse(w http.ResponseWriter) error +} + +type PostUserInfo200JSONResponse UserInfo + +func (response PostUserInfo200JSONResponse) VisitPostUserInfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PostUserInfo401Response struct { +} + +func (response PostUserInfo401Response) VisitPostUserInfoResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil } // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // JSON Web Key Set + // (GET /.well-known/jwks.json) + GetJWKS(ctx context.Context, request GetJWKSRequestObject) (GetJWKSResponseObject, error) + // OpenID Provider Configuration + // (GET /.well-known/openid-configuration) + GetOpenIDConfiguration(ctx context.Context, request GetOpenIDConfigurationRequestObject) (GetOpenIDConfigurationResponseObject, error) + // クライアント一覧取得 + // (GET /api/v1/admin/clients) + GetClients(ctx context.Context, request GetClientsRequestObject) (GetClientsResponseObject, error) + // クライアント作成 + // (POST /api/v1/admin/clients) + CreateClient(ctx context.Context, request CreateClientRequestObject) (CreateClientResponseObject, error) + // クライアント削除 + // (DELETE /api/v1/admin/clients/{clientId}) + DeleteClient(ctx context.Context, request DeleteClientRequestObject) (DeleteClientResponseObject, error) + // クライアント取得 + // (GET /api/v1/admin/clients/{clientId}) + GetClient(ctx context.Context, request GetClientRequestObject) (GetClientResponseObject, error) + // クライアント更新 + // (PUT /api/v1/admin/clients/{clientId}) + UpdateClient(ctx context.Context, request UpdateClientRequestObject) (UpdateClientResponseObject, error) + // クライアントシークレット再生成 + // (POST /api/v1/admin/clients/{clientId}/secret) + RegenerateClientSecret(ctx context.Context, request RegenerateClientSecretRequestObject) (RegenerateClientSecretResponseObject, error) + // 認可エンドポイント + // (GET /oauth2/authorize) + Authorize(ctx context.Context, request AuthorizeRequestObject) (AuthorizeResponseObject, error) + // トークンエンドポイント + // (POST /oauth2/token) + Token(ctx context.Context, request TokenRequestObject) (TokenResponseObject, error) + // UserInfo エンドポイント (GET) + // (GET /oauth2/userinfo) + GetUserInfo(ctx context.Context, request GetUserInfoRequestObject) (GetUserInfoResponseObject, error) + // UserInfo エンドポイント (POST) + // (POST /oauth2/userinfo) + PostUserInfo(ctx context.Context, request PostUserInfoRequestObject) (PostUserInfoResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -58,3 +689,311 @@ type strictHandler struct { ssi StrictServerInterface middlewares []StrictMiddlewareFunc } + +// GetJWKS operation middleware +func (sh *strictHandler) GetJWKS(ctx echo.Context) error { + var request GetJWKSRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetJWKS(ctx.Request().Context(), request.(GetJWKSRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetJWKS") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetJWKSResponseObject); ok { + return validResponse.VisitGetJWKSResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// GetOpenIDConfiguration operation middleware +func (sh *strictHandler) GetOpenIDConfiguration(ctx echo.Context) error { + var request GetOpenIDConfigurationRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetOpenIDConfiguration(ctx.Request().Context(), request.(GetOpenIDConfigurationRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetOpenIDConfiguration") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetOpenIDConfigurationResponseObject); ok { + return validResponse.VisitGetOpenIDConfigurationResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// GetClients operation middleware +func (sh *strictHandler) GetClients(ctx echo.Context) error { + var request GetClientsRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetClients(ctx.Request().Context(), request.(GetClientsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetClients") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetClientsResponseObject); ok { + return validResponse.VisitGetClientsResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// CreateClient operation middleware +func (sh *strictHandler) CreateClient(ctx echo.Context) error { + var request CreateClientRequestObject + + var body CreateClientJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.CreateClient(ctx.Request().Context(), request.(CreateClientRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateClient") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(CreateClientResponseObject); ok { + return validResponse.VisitCreateClientResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// DeleteClient operation middleware +func (sh *strictHandler) DeleteClient(ctx echo.Context, clientId openapi_types.UUID) error { + var request DeleteClientRequestObject + + request.ClientId = clientId + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.DeleteClient(ctx.Request().Context(), request.(DeleteClientRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DeleteClient") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(DeleteClientResponseObject); ok { + return validResponse.VisitDeleteClientResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// GetClient operation middleware +func (sh *strictHandler) GetClient(ctx echo.Context, clientId openapi_types.UUID) error { + var request GetClientRequestObject + + request.ClientId = clientId + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetClient(ctx.Request().Context(), request.(GetClientRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetClient") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetClientResponseObject); ok { + return validResponse.VisitGetClientResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// UpdateClient operation middleware +func (sh *strictHandler) UpdateClient(ctx echo.Context, clientId openapi_types.UUID) error { + var request UpdateClientRequestObject + + request.ClientId = clientId + + var body UpdateClientJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.UpdateClient(ctx.Request().Context(), request.(UpdateClientRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateClient") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(UpdateClientResponseObject); ok { + return validResponse.VisitUpdateClientResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// RegenerateClientSecret operation middleware +func (sh *strictHandler) RegenerateClientSecret(ctx echo.Context, clientId openapi_types.UUID) error { + var request RegenerateClientSecretRequestObject + + request.ClientId = clientId + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.RegenerateClientSecret(ctx.Request().Context(), request.(RegenerateClientSecretRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RegenerateClientSecret") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(RegenerateClientSecretResponseObject); ok { + return validResponse.VisitRegenerateClientSecretResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// Authorize operation middleware +func (sh *strictHandler) Authorize(ctx echo.Context, params AuthorizeParams) error { + var request AuthorizeRequestObject + + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.Authorize(ctx.Request().Context(), request.(AuthorizeRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Authorize") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(AuthorizeResponseObject); ok { + return validResponse.VisitAuthorizeResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// Token operation middleware +func (sh *strictHandler) Token(ctx echo.Context) error { + var request TokenRequestObject + + if form, err := ctx.FormParams(); err == nil { + var body TokenFormdataRequestBody + if err := runtime.BindForm(&body, form, nil, nil); err != nil { + return err + } + request.Body = &body + } else { + return err + } + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.Token(ctx.Request().Context(), request.(TokenRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Token") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(TokenResponseObject); ok { + return validResponse.VisitTokenResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// GetUserInfo operation middleware +func (sh *strictHandler) GetUserInfo(ctx echo.Context) error { + var request GetUserInfoRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetUserInfo(ctx.Request().Context(), request.(GetUserInfoRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetUserInfo") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetUserInfoResponseObject); ok { + return validResponse.VisitGetUserInfoResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// PostUserInfo operation middleware +func (sh *strictHandler) PostUserInfo(ctx echo.Context) error { + var request PostUserInfoRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.PostUserInfo(ctx.Request().Context(), request.(PostUserInfoRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostUserInfo") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(PostUserInfoResponseObject); ok { + return validResponse.VisitPostUserInfoResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} diff --git a/internal/router/v1/handler.go b/internal/router/v1/handler.go new file mode 100644 index 0000000..c973eda --- /dev/null +++ b/internal/router/v1/handler.go @@ -0,0 +1,52 @@ +package v1 + +import ( + "crypto/rsa" + "net/http" + "strings" + + "github.com/gorilla/sessions" + "github.com/ory/fosite" + + "github.com/traPtitech/portal-oidc/internal/usecase" +) + +type Handler struct { + clientUseCase usecase.ClientUseCase + oauth2 fosite.OAuth2Provider + userUseCase usecase.UserUseCase + sessions *sessions.CookieStore + config OAuthConfig +} + +type OAuthConfig struct { + Issuer string + SessionSecret []byte // #nosec G117 -- internal config, not serialized + PrivateKey *rsa.PrivateKey + Environment string + TestUserID string +} + +func NewHandler( + clientUseCase usecase.ClientUseCase, + oauth2 fosite.OAuth2Provider, + userUseCase usecase.UserUseCase, + config OAuthConfig, +) *Handler { + store := sessions.NewCookieStore(config.SessionSecret) + store.Options = &sessions.Options{ + Path: "/", + MaxAge: 600, + HttpOnly: true, + Secure: strings.HasPrefix(config.Issuer, "https://"), + SameSite: http.SameSiteLaxMode, + } + + return &Handler{ + clientUseCase: clientUseCase, + oauth2: oauth2, + userUseCase: userUseCase, + sessions: store, + config: config, + } +} diff --git a/internal/router/v1/oauth.go b/internal/router/v1/oauth.go new file mode 100644 index 0000000..24f2389 --- /dev/null +++ b/internal/router/v1/oauth.go @@ -0,0 +1,249 @@ +package v1 + +import ( + "crypto/sha256" + "encoding/base64" + "errors" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/go-jose/go-jose/v4" + "github.com/labstack/echo/v4" + "github.com/ory/fosite" + + "github.com/traPtitech/portal-oidc/internal/repository/oauth" + "github.com/traPtitech/portal-oidc/internal/router/v1/gen" +) + +func (h *Handler) Authorize(ctx echo.Context, params gen.AuthorizeParams) error { + c := ctx.Request().Context() + rw := ctx.Response() + req := ctx.Request() + + ar, err := h.oauth2.NewAuthorizeRequest(c, req) + if err != nil { + h.oauth2.WriteAuthorizeError(c, rw, ar, err) + return nil + } + + prompt := ar.GetRequestForm().Get("prompt") + returnURL := req.URL.String() + + if h.config.Environment != "production" { + return h.completeAuthorize(ctx, ar, h.config.TestUserID, time.Now()) + } + + info, authenticated := h.getAuthInfo(ctx) + + switch prompt { + case "none": + if !authenticated { + h.oauth2.WriteAuthorizeError(c, rw, ar, fosite.ErrLoginRequired) + return nil + } + case "login": + if !authenticated || !h.isReauthCompleted(ctx, info.AuthTime) { + return h.redirectToLogin(ctx, returnURL) + } + default: + if !authenticated { + return ctx.Redirect(http.StatusFound, "/login?return_url="+url.QueryEscape(returnURL)) + } + } + + if h.isMaxAgeExpired(ar, info.AuthTime) && !h.isReauthCompleted(ctx, info.AuthTime) { + return h.redirectToLogin(ctx, returnURL) + } + + return h.completeAuthorize(ctx, ar, info.UserID, info.AuthTime) +} + +func (h *Handler) completeAuthorize(ctx echo.Context, ar fosite.AuthorizeRequester, userID string, authTime time.Time) error { + c := ctx.Request().Context() + rw := ctx.Response() + + session := oauth.NewSession(userID, authTime) + for _, scope := range ar.GetRequestedScopes() { + ar.GrantScope(scope) + } + + response, err := h.oauth2.NewAuthorizeResponse(c, ar, session) + if err != nil { + h.oauth2.WriteAuthorizeError(c, rw, ar, err) + return nil + } + + h.oauth2.WriteAuthorizeResponse(c, rw, ar, response) + return nil +} + +func (h *Handler) isReauthCompleted(ctx echo.Context, authTime time.Time) bool { + session, err := h.sessions.Get(ctx.Request(), sessionName) + if err != nil { + return false + } + + reqAt, ok := session.Values["reauth_requested_at"].(int64) + if !ok { + return false + } + + return authTime.Unix() > reqAt +} + +func (h *Handler) redirectToLogin(ctx echo.Context, returnURL string) error { + session, err := h.sessions.Get(ctx.Request(), sessionName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get session") + } + + session.Values["reauth_requested_at"] = time.Now().Unix() + session.Values["authenticated"] = false + + if err := session.Save(ctx.Request(), ctx.Response()); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to save session") + } + + return ctx.Redirect(http.StatusFound, "/login?return_url="+url.QueryEscape(returnURL)) +} + +func (h *Handler) isMaxAgeExpired(ar fosite.AuthorizeRequester, authTime time.Time) bool { + maxAgeStr := ar.GetRequestForm().Get("max_age") + if maxAgeStr == "" { + return false + } + maxAge, err := strconv.ParseInt(maxAgeStr, 10, 64) + if err != nil { + return false + } + return time.Since(authTime) > time.Duration(maxAge)*time.Second +} + +func (h *Handler) Token(ctx echo.Context) error { + c := ctx.Request().Context() + rw := ctx.Response() + req := ctx.Request() + + session := oauth.NewSession("", time.Time{}) + accessRequest, err := h.oauth2.NewAccessRequest(c, req, session) + if err != nil { + h.oauth2.WriteAccessError(c, rw, accessRequest, err) + return nil + } + + for _, scope := range accessRequest.GetRequestedScopes() { + accessRequest.GrantScope(scope) + } + + response, err := h.oauth2.NewAccessResponse(c, accessRequest) + if err != nil { + h.oauth2.WriteAccessError(c, rw, accessRequest, err) + return nil + } + + h.oauth2.WriteAccessResponse(c, rw, accessRequest, response) + return nil +} + +func (h *Handler) GetUserInfo(ctx echo.Context) error { + token, err := h.extractBearerToken(ctx) + if err != nil { + return ctx.JSON(http.StatusUnauthorized, gen.OAuthError{Error: gen.InvalidRequest}) + } + return h.handleUserInfo(ctx, token) +} + +func (h *Handler) PostUserInfo(ctx echo.Context) error { + // RFC 6750: POST can use Authorization header OR form body + token, err := h.extractBearerToken(ctx) + if err != nil { + // Try form body (application/x-www-form-urlencoded) + token = ctx.FormValue("access_token") + if token == "" { + return ctx.JSON(http.StatusUnauthorized, gen.OAuthError{Error: gen.InvalidRequest}) + } + } + return h.handleUserInfo(ctx, token) +} + +func (h *Handler) extractBearerToken(ctx echo.Context) (string, error) { + authHeader := ctx.Request().Header.Get("Authorization") + if authHeader == "" { + return "", errors.New("no authorization header") + } + + // RFC 6750: The access token type is case-insensitive + if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") { + return authHeader[7:], nil // len("bearer ") == 7 + } + return "", errors.New("invalid authorization header") +} + +func (h *Handler) handleUserInfo(ctx echo.Context, token string) error { + c := ctx.Request().Context() + + _, ar, err := h.oauth2.IntrospectToken(c, token, fosite.AccessToken, oauth.NewSession("", time.Time{})) + if err != nil { + return ctx.JSON(http.StatusUnauthorized, gen.OAuthError{Error: gen.InvalidGrant}) + } + + sub := ar.GetSession().GetSubject() + info := gen.UserInfo{Sub: sub} + + if h.userUseCase != nil && ar.GetGrantedScopes().Has("profile") { + user, userErr := h.userUseCase.GetByID(c, sub) + if userErr == nil { + info.Name = &user.TrapID + info.PreferredUsername = &user.TrapID + } + } + + return ctx.JSON(http.StatusOK, info) +} + +func (h *Handler) GetJWKS(ctx echo.Context) error { + if h.config.PrivateKey == nil { + return echo.NewHTTPError(http.StatusInternalServerError, "signing key not configured") + } + pubKey := &h.config.PrivateKey.PublicKey + + hash := sha256.Sum256(pubKey.N.Bytes()) + kid := base64.RawURLEncoding.EncodeToString(hash[:8]) + + jwk := jose.JSONWebKey{ + Key: pubKey, + KeyID: kid, + Algorithm: string(jose.RS256), + Use: "sig", + } + + return ctx.JSON(http.StatusOK, map[string]interface{}{ + "keys": []jose.JSONWebKey{jwk}, + }) +} + +func (h *Handler) GetOpenIDConfiguration(ctx echo.Context) error { + issuer := strings.TrimRight(h.config.Issuer, "/") + scopesSupported := []string{"openid", "profile", "email"} + claimsSupported := []string{"sub", "name", "preferred_username", "email", "email_verified"} + codeChallengeMethodsSupported := []string{"S256", "plain"} + tokenEndpointAuthMethodsSupported := []string{"client_secret_basic", "client_secret_post"} + + return ctx.JSON(http.StatusOK, gen.OpenIDConfiguration{ + Issuer: issuer, + AuthorizationEndpoint: issuer + "/oauth2/authorize", + TokenEndpoint: issuer + "/oauth2/token", + UserinfoEndpoint: issuer + "/oauth2/userinfo", + JwksUri: issuer + "/.well-known/jwks.json", + ResponseTypesSupported: []string{"code"}, + SubjectTypesSupported: []string{"public"}, + IdTokenSigningAlgValuesSupported: []string{"RS256"}, + ScopesSupported: &scopesSupported, + ClaimsSupported: &claimsSupported, + CodeChallengeMethodsSupported: &codeChallengeMethodsSupported, + TokenEndpointAuthMethodsSupported: &tokenEndpointAuthMethodsSupported, + }) +} diff --git a/internal/testutil/root.go b/internal/testutil/root.go new file mode 100644 index 0000000..e03a70d --- /dev/null +++ b/internal/testutil/root.go @@ -0,0 +1,26 @@ +package testutil + +import ( + "os" + "path/filepath" +) + +// FindProjectRoot finds the project root by looking for go.mod +func FindProjectRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + + parent := filepath.Dir(dir) + if parent == dir { + return "", os.ErrNotExist + } + dir = parent + } +} diff --git a/internal/usecase/client.go b/internal/usecase/client.go new file mode 100644 index 0000000..315ac56 --- /dev/null +++ b/internal/usecase/client.go @@ -0,0 +1,161 @@ +package usecase + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "time" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository" +) + +var ErrClientNotFound = errors.New("client not found") + +type ClientUseCase interface { + Create(ctx context.Context, name string, clientType domain.ClientType, redirectURIs []string) (*domain.ClientWithSecret, error) + Get(ctx context.Context, clientID uuid.UUID) (*domain.Client, error) + List(ctx context.Context) ([]*domain.Client, error) + Update(ctx context.Context, clientID uuid.UUID, name string, clientType domain.ClientType, redirectURIs []string) (*domain.Client, error) + RegenerateSecret(ctx context.Context, clientID uuid.UUID) (string, error) + Delete(ctx context.Context, clientID uuid.UUID) error +} + +type clientUseCase struct { + repo repository.ClientRepository +} + +func NewClientUseCase(repo repository.ClientRepository) ClientUseCase { + return &clientUseCase{repo: repo} +} + +func (u *clientUseCase) Create(ctx context.Context, name string, clientType domain.ClientType, redirectURIs []string) (*domain.ClientWithSecret, error) { + clientID := uuid.New() + + secret, err := generateSecret() + if err != nil { + return nil, err + } + + secretHash, err := hashSecret(secret) + if err != nil { + return nil, err + } + + now := time.Now() + client := &domain.Client{ + ClientID: clientID, + Name: name, + ClientType: clientType, + RedirectURIs: redirectURIs, + CreatedAt: now, + UpdatedAt: now, + } + + if err := u.repo.Create(ctx, client, secretHash); err != nil { + return nil, err + } + + createdClient, err := u.repo.Get(ctx, clientID) + if err != nil { + return nil, err + } + + return &domain.ClientWithSecret{ + Client: *createdClient, + ClientSecret: secret, + }, nil +} + +func (u *clientUseCase) Get(ctx context.Context, clientID uuid.UUID) (*domain.Client, error) { + client, err := u.repo.Get(ctx, clientID) + if err != nil { + if errors.Is(err, repository.ErrClientNotFound) { + return nil, ErrClientNotFound + } + return nil, err + } + return client, nil +} + +func (u *clientUseCase) List(ctx context.Context) ([]*domain.Client, error) { + return u.repo.List(ctx) +} + +func (u *clientUseCase) Update(ctx context.Context, clientID uuid.UUID, name string, clientType domain.ClientType, redirectURIs []string) (*domain.Client, error) { + existing, err := u.repo.Get(ctx, clientID) + if err != nil { + if errors.Is(err, repository.ErrClientNotFound) { + return nil, ErrClientNotFound + } + return nil, err + } + + existing.Name = name + existing.ClientType = clientType + existing.RedirectURIs = redirectURIs + + if err := u.repo.Update(ctx, existing); err != nil { + return nil, err + } + + return u.repo.Get(ctx, clientID) +} + +func (u *clientUseCase) RegenerateSecret(ctx context.Context, clientID uuid.UUID) (string, error) { + _, err := u.repo.Get(ctx, clientID) + if err != nil { + if errors.Is(err, repository.ErrClientNotFound) { + return "", ErrClientNotFound + } + return "", err + } + + secret, err := generateSecret() + if err != nil { + return "", err + } + + secretHash, err := hashSecret(secret) + if err != nil { + return "", err + } + + if err := u.repo.UpdateSecret(ctx, clientID, secretHash); err != nil { + return "", err + } + + return secret, nil +} + +func (u *clientUseCase) Delete(ctx context.Context, clientID uuid.UUID) error { + _, err := u.repo.Get(ctx, clientID) + if err != nil { + if errors.Is(err, repository.ErrClientNotFound) { + return ErrClientNotFound + } + return err + } + + return u.repo.Delete(ctx, clientID) +} + +func generateSecret() (string, error) { + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(bytes), nil +} + +func hashSecret(secret string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} diff --git a/internal/usecase/client_test.go b/internal/usecase/client_test.go new file mode 100644 index 0000000..973ecc9 --- /dev/null +++ b/internal/usecase/client_test.go @@ -0,0 +1,297 @@ +package usecase + +import ( + "context" + "sync" + "testing" + + "github.com/google/uuid" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository" +) + +// mockClientRepository is an in-memory implementation for testing +type mockClientRepository struct { + mu sync.RWMutex + clients map[uuid.UUID]*clientWithHash +} + +type clientWithHash struct { + client *domain.Client + secretHash string +} + +func newMockClientRepository() *mockClientRepository { + return &mockClientRepository{ + clients: make(map[uuid.UUID]*clientWithHash), + } +} + +func (r *mockClientRepository) Create(ctx context.Context, client *domain.Client, secretHash string) error { + r.mu.Lock() + defer r.mu.Unlock() + r.clients[client.ClientID] = &clientWithHash{ + client: client, + secretHash: secretHash, + } + return nil +} + +func (r *mockClientRepository) Get(ctx context.Context, clientID uuid.UUID) (*domain.Client, error) { + r.mu.RLock() + defer r.mu.RUnlock() + if c, ok := r.clients[clientID]; ok { + return c.client, nil + } + return nil, repository.ErrClientNotFound +} + +func (r *mockClientRepository) List(ctx context.Context) ([]*domain.Client, error) { + r.mu.RLock() + defer r.mu.RUnlock() + clients := make([]*domain.Client, 0, len(r.clients)) + for _, c := range r.clients { + clients = append(clients, c.client) + } + return clients, nil +} + +func (r *mockClientRepository) Update(ctx context.Context, client *domain.Client) error { + r.mu.Lock() + defer r.mu.Unlock() + if c, ok := r.clients[client.ClientID]; ok { + c.client = client + return nil + } + return repository.ErrClientNotFound +} + +func (r *mockClientRepository) UpdateSecret(ctx context.Context, clientID uuid.UUID, secretHash string) error { + r.mu.Lock() + defer r.mu.Unlock() + if c, ok := r.clients[clientID]; ok { + c.secretHash = secretHash + return nil + } + return repository.ErrClientNotFound +} + +func (r *mockClientRepository) Delete(ctx context.Context, clientID uuid.UUID) error { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.clients, clientID) + return nil +} + +func (r *mockClientRepository) getSecretHash(clientID uuid.UUID) string { + r.mu.RLock() + defer r.mu.RUnlock() + if c, ok := r.clients[clientID]; ok { + return c.secretHash + } + return "" +} + +func (r *mockClientRepository) GetWithSecretHash(ctx context.Context, clientID uuid.UUID) (*domain.Client, string, error) { + r.mu.RLock() + defer r.mu.RUnlock() + if c, ok := r.clients[clientID]; ok { + return c.client, c.secretHash, nil + } + return nil, "", repository.ErrClientNotFound +} + +func TestClientUseCase_Create(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + created, err := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + if created.ClientID == uuid.Nil { + t.Error("ClientID should not be nil") + } + if created.ClientSecret == "" { + t.Error("ClientSecret should not be empty") + } + if created.Name != "test-client" { + t.Errorf("Name = %q, want %q", created.Name, "test-client") + } + if created.ClientType != domain.ClientTypeConfidential { + t.Errorf("ClientType = %q, want %q", created.ClientType, domain.ClientTypeConfidential) + } + if len(created.RedirectURIs) != 1 || created.RedirectURIs[0] != "http://localhost:3000/callback" { + t.Errorf("RedirectURIs = %v, want [http://localhost:3000/callback]", created.RedirectURIs) + } +} + +func TestClientUseCase_Get(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) + + got, err := uc.Get(ctx, created.ClientID) + if err != nil { + t.Fatalf("Get failed: %v", err) + } + + if got.ClientID != created.ClientID { + t.Errorf("ClientID = %s, want %s", got.ClientID, created.ClientID) + } + if got.Name != created.Name { + t.Errorf("Name = %q, want %q", got.Name, created.Name) + } +} + +func TestClientUseCase_Get_NotFound(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + _, err := uc.Get(ctx, uuid.New()) + if err != ErrClientNotFound { + t.Errorf("err = %v, want ErrClientNotFound", err) + } +} + +func TestClientUseCase_List(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + // Empty list + list, err := uc.List(ctx) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(list) != 0 { + t.Errorf("len(list) = %d, want 0", len(list)) + } + + // Create clients + _, err = uc.Create(ctx, "client1", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) + if err != nil { + t.Fatalf("Create client1 failed: %v", err) + } + _, err = uc.Create(ctx, "client2", domain.ClientTypePublic, []string{"http://localhost:3001/callback"}) + if err != nil { + t.Fatalf("Create client2 failed: %v", err) + } + + list, err = uc.List(ctx) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(list) != 2 { + t.Errorf("len(list) = %d, want 2", len(list)) + } +} + +func TestClientUseCase_Update(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + created, _ := uc.Create(ctx, "original", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) + + updated, err := uc.Update(ctx, created.ClientID, "updated", domain.ClientTypePublic, []string{"http://localhost:4000/callback"}) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + + if updated.ClientID != created.ClientID { + t.Error("ClientID should not change") + } + if updated.Name != "updated" { + t.Errorf("Name = %q, want %q", updated.Name, "updated") + } + if updated.ClientType != domain.ClientTypePublic { + t.Errorf("ClientType = %q, want %q", updated.ClientType, domain.ClientTypePublic) + } + if len(updated.RedirectURIs) != 1 || updated.RedirectURIs[0] != "http://localhost:4000/callback" { + t.Errorf("RedirectURIs = %v, want [http://localhost:4000/callback]", updated.RedirectURIs) + } +} + +func TestClientUseCase_Update_NotFound(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + _, err := uc.Update(ctx, uuid.New(), "name", domain.ClientTypeConfidential, []string{"http://localhost:3000"}) + if err != ErrClientNotFound { + t.Errorf("err = %v, want ErrClientNotFound", err) + } +} + +func TestClientUseCase_Delete(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) + + err := uc.Delete(ctx, created.ClientID) + if err != nil { + t.Fatalf("Delete failed: %v", err) + } + + _, err = uc.Get(ctx, created.ClientID) + if err != ErrClientNotFound { + t.Errorf("err = %v, want ErrClientNotFound", err) + } +} + +func TestClientUseCase_Delete_NotFound(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + err := uc.Delete(ctx, uuid.New()) + if err != ErrClientNotFound { + t.Errorf("err = %v, want ErrClientNotFound", err) + } +} + +func TestClientUseCase_RegenerateSecret(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) + originalHash := repo.getSecretHash(created.ClientID) + + newSecret, err := uc.RegenerateSecret(ctx, created.ClientID) + if err != nil { + t.Fatalf("RegenerateSecret failed: %v", err) + } + + if newSecret == "" { + t.Error("new secret should not be empty") + } + if newSecret == created.ClientSecret { + t.Error("new secret should be different from original") + } + + newHash := repo.getSecretHash(created.ClientID) + if newHash == originalHash { + t.Error("secret hash should be updated") + } +} + +func TestClientUseCase_RegenerateSecret_NotFound(t *testing.T) { + repo := newMockClientRepository() + uc := NewClientUseCase(repo) + ctx := context.Background() + + _, err := uc.RegenerateSecret(ctx, uuid.New()) + if err != ErrClientNotFound { + t.Errorf("err = %v, want ErrClientNotFound", err) + } +} diff --git a/internal/usecase/user.go b/internal/usecase/user.go new file mode 100644 index 0000000..5a81b41 --- /dev/null +++ b/internal/usecase/user.go @@ -0,0 +1,120 @@ +package usecase + +import ( + "context" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "strconv" + "strings" + + "golang.org/x/crypto/pbkdf2" + + "github.com/traPtitech/portal-oidc/internal/domain" + "github.com/traPtitech/portal-oidc/internal/repository" +) + +var ( + ErrInvalidPassword = errors.New("invalid password") + ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm") + ErrUserNotActive = errors.New("user is not active") + ErrUserNotFound = errors.New("user not found") +) + +type UserUseCase interface { + Authenticate(ctx context.Context, trapID, password string) (*domain.User, error) + GetByID(ctx context.Context, id string) (*domain.User, error) +} + +type userUseCase struct { + repo repository.UserRepository +} + +func NewUserUseCase(repo repository.UserRepository) UserUseCase { + return &userUseCase{repo: repo} +} + +func (u *userUseCase) GetByID(ctx context.Context, id string) (*domain.User, error) { + user, err := u.repo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, repository.ErrUserNotFound) { + return nil, ErrUserNotFound + } + return nil, err + } + return user, nil +} + +func (u *userUseCase) Authenticate(ctx context.Context, trapID, password string) (*domain.User, error) { + user, err := u.repo.GetByTrapID(ctx, trapID) + if err != nil { + if errors.Is(err, repository.ErrUserNotFound) { + return nil, ErrUserNotFound + } + return nil, err + } + + if err := verifyPassword(password, user.PasswordHash); err != nil { + return nil, err + } + + statuses, err := u.repo.ListStatuses(ctx, user.ID) + if err != nil { + return nil, err + } + + if len(statuses) > 0 { + isActive := false + for _, status := range statuses { + if status == "active" { + isActive = true + break + } + } + if !isActive { + return nil, ErrUserNotActive + } + } + + return &user.User, nil +} + +func verifyPassword(password, storedHash string) error { + parts := strings.Split(storedHash, "$") + if len(parts) != 4 { + return fmt.Errorf("%w: invalid hash format", ErrUnsupportedHashAlgorithm) + } + + switch parts[0] { + case "pbkdf2_sha512": + return verifyPBKDF2SHA512(password, parts[1], parts[2], parts[3]) + default: + return fmt.Errorf("%w: %s", ErrUnsupportedHashAlgorithm, parts[0]) + } +} + +func verifyPBKDF2SHA512(password, iterationsStr, saltB64, hashB64 string) error { + iterations, err := strconv.Atoi(iterationsStr) + if err != nil { + return fmt.Errorf("%w: invalid iterations: %w", ErrUnsupportedHashAlgorithm, err) + } + + salt, err := base64.StdEncoding.DecodeString(saltB64) + if err != nil { + return fmt.Errorf("%w: invalid salt: %w", ErrUnsupportedHashAlgorithm, err) + } + + expectedHash, err := base64.StdEncoding.DecodeString(hashB64) + if err != nil { + return fmt.Errorf("%w: invalid hash: %w", ErrUnsupportedHashAlgorithm, err) + } + + computedHash := pbkdf2.Key([]byte(password), salt, iterations, len(expectedHash), sha512.New) + + if subtle.ConstantTimeCompare(computedHash, expectedHash) != 1 { + return ErrInvalidPassword + } + return nil +} diff --git a/mise.toml b/mise.toml index 3105baa..e51c182 100644 --- a/mise.toml +++ b/mise.toml @@ -5,6 +5,7 @@ atlas = "1.0.0" tbls = "1.92.3" pre-commit = "4.5.1" "go:github.com/sqlc-dev/sqlc/cmd/sqlc" = "1.30.0" +"go:github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" = "2.5.1" # ============================================================================= # Development @@ -59,8 +60,8 @@ outputs = [ description = "OpenAPIでサーバー/モデル生成" dir = "api" run = """ -go tool oapi-codegen --config oapi.server.yaml openapi.yaml -go tool oapi-codegen --config oapi.models.yaml openapi.yaml +oapi-codegen --config oapi.server.yaml openapi.yaml +oapi-codegen --config oapi.models.yaml openapi.yaml """ sources = [ "openapi.yaml",