From d10ac394632d949d9b139140d11bee6ff85c1dba Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:47:38 +0900 Subject: [PATCH 01/26] feat: add client table schema and queries --- db/query/oidc.sql | 5 ++++- db/schema.sql | 2 +- internal/repository/oidc/db.go | 10 ++++++++++ internal/repository/oidc/oidc.sql.go | 9 +++++++++ internal/repository/oidc/querier.go | 1 + 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/db/query/oidc.sql b/db/query/oidc.sql index 260c637..250a0fe 100644 --- a/db/query/oidc.sql +++ b/db/query/oidc.sql @@ -28,4 +28,7 @@ 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; \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index 2639f4f..0a40901 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, diff --git a/internal/repository/oidc/db.go b/internal/repository/oidc/db.go index e6fea6b..70cdeb7 100644 --- a/internal/repository/oidc/db.go +++ b/internal/repository/oidc/db.go @@ -27,6 +27,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.createClientStmt, err = db.PrepareContext(ctx, createClient); err != nil { return nil, fmt.Errorf("error preparing query CreateClient: %w", err) } + if q.deleteAllClientsStmt, err = db.PrepareContext(ctx, deleteAllClients); err != nil { + return nil, fmt.Errorf("error preparing query DeleteAllClients: %w", err) + } if q.deleteClientStmt, err = db.PrepareContext(ctx, deleteClient); err != nil { return nil, fmt.Errorf("error preparing query DeleteClient: %w", err) } @@ -52,6 +55,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing createClientStmt: %w", cerr) } } + if q.deleteAllClientsStmt != nil { + if cerr := q.deleteAllClientsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteAllClientsStmt: %w", cerr) + } + } if q.deleteClientStmt != nil { if cerr := q.deleteClientStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteClientStmt: %w", cerr) @@ -117,6 +125,7 @@ type Queries struct { db DBTX tx *sql.Tx createClientStmt *sql.Stmt + deleteAllClientsStmt *sql.Stmt deleteClientStmt *sql.Stmt getClientStmt *sql.Stmt listClientsStmt *sql.Stmt @@ -129,6 +138,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { db: tx, tx: tx, createClientStmt: q.createClientStmt, + deleteAllClientsStmt: q.deleteAllClientsStmt, deleteClientStmt: q.deleteClientStmt, getClientStmt: q.getClientStmt, listClientsStmt: q.listClientsStmt, diff --git a/internal/repository/oidc/oidc.sql.go b/internal/repository/oidc/oidc.sql.go index 31420be..25ae339 100644 --- a/internal/repository/oidc/oidc.sql.go +++ b/internal/repository/oidc/oidc.sql.go @@ -42,6 +42,15 @@ func (q *Queries) CreateClient(ctx context.Context, arg CreateClientParams) erro 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 deleteClient = `-- name: DeleteClient :exec DELETE FROM clients WHERE client_id = ? ` diff --git a/internal/repository/oidc/querier.go b/internal/repository/oidc/querier.go index 951d3bd..d4a54ea 100644 --- a/internal/repository/oidc/querier.go +++ b/internal/repository/oidc/querier.go @@ -11,6 +11,7 @@ import ( type Querier interface { // Client queries CreateClient(ctx context.Context, arg CreateClientParams) error + DeleteAllClients(ctx context.Context) error DeleteClient(ctx context.Context, clientID string) error GetClient(ctx context.Context, clientID string) (Client, error) ListClients(ctx context.Context) ([]Client, error) From 25cdca1ab48791058eec0e12b84da76f91527288 Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:47:42 +0900 Subject: [PATCH 02/26] feat: add client CRUD API definition and generated code --- api/openapi.yaml | 225 +++++++++++++- internal/router/v1/gen/models.go | 75 +++++ internal/router/v1/gen/server.go | 491 +++++++++++++++++++++++++++++++ 3 files changed, 789 insertions(+), 2 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index f547591..a41b074 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -13,7 +13,228 @@ 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 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: 新しいクライアントシークレット diff --git a/internal/router/v1/gen/models.go b/internal/router/v1/gen/models.go index be11371..e9b0aff 100644 --- a/internal/router/v1/gen/models.go +++ b/internal/router/v1/gen/models.go @@ -2,3 +2,78 @@ // // 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" +) + +// Defines values for ClientType. +const ( + Confidential ClientType = "confidential" + Public ClientType = "public" +) + +// 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"` +} + +// CreateClientJSONRequestBody defines body for CreateClient for application/json ContentType. +type CreateClientJSONRequestBody = ClientCreate + +// UpdateClientJSONRequestBody defines body for UpdateClient for application/json ContentType. +type UpdateClientJSONRequestBody = ClientUpdate diff --git a/internal/router/v1/gen/server.go b/internal/router/v1/gen/server.go index 7712313..436a600 100644 --- a/internal/router/v1/gen/server.go +++ b/internal/router/v1/gen/server.go @@ -4,12 +4,37 @@ 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 { + // クライアント一覧取得 + // (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 } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -17,6 +42,88 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } +// 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 +} + // 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 +148,236 @@ 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+"/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) + +} + +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 } // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // クライアント一覧取得 + // (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) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -58,3 +391,161 @@ type strictHandler struct { ssi StrictServerInterface middlewares []StrictMiddlewareFunc } + +// 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 +} From bde5d01d7453d3db6d64d2cd040c1a9158731f03 Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:47:46 +0900 Subject: [PATCH 03/26] feat: implement client CRUD domain, repository, usecase, and router --- internal/domain/client.go | 28 +++ internal/repository/client.go | 129 ++++++++++++++ internal/router/v1/client.go | 130 ++++++++++++++ internal/router/v1/handler.go | 13 ++ internal/testutil/root.go | 26 +++ internal/usecase/client.go | 161 +++++++++++++++++ internal/usecase/client_test.go | 297 ++++++++++++++++++++++++++++++++ 7 files changed, 784 insertions(+) create mode 100644 internal/domain/client.go create mode 100644 internal/repository/client.go create mode 100644 internal/router/v1/client.go create mode 100644 internal/router/v1/handler.go create mode 100644 internal/testutil/root.go create mode 100644 internal/usecase/client.go create mode 100644 internal/usecase/client_test.go diff --git a/internal/domain/client.go b/internal/domain/client.go new file mode 100644 index 0000000..d8d64e4 --- /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 +} diff --git a/internal/repository/client.go b/internal/repository/client.go new file mode 100644 index 0000000..90db1fb --- /dev/null +++ b/internal/repository/client.go @@ -0,0 +1,129 @@ +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) + 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) 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/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/handler.go b/internal/router/v1/handler.go new file mode 100644 index 0000000..6386d44 --- /dev/null +++ b/internal/router/v1/handler.go @@ -0,0 +1,13 @@ +package v1 + +import "github.com/traPtitech/portal-oidc/internal/usecase" + +type Handler struct { + clientUseCase usecase.ClientUseCase +} + +func NewHandler(clientUseCase usecase.ClientUseCase) *Handler { + return &Handler{ + clientUseCase: clientUseCase, + } +} 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) + } +} From fc3701823786514862aef1992042eb4cedd37b2b Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:47:52 +0900 Subject: [PATCH 04/26] feat: integrate client usecase into server --- cmd/serve.go | 11 ++++++----- go.mod | 4 +++- go.sum | 9 +++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index b3d64a0..dd278de 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -10,21 +10,22 @@ 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/oidc" + 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) if err != nil { return nil, err } - handler := &Handler{queries: queries} + clientRepo := repository.NewClientRepository(queries) + clientUC := usecase.NewClientUseCase(clientRepo) + handler := v1.NewHandler(clientUC) e := echo.New() e.Use(middleware.Recover()) diff --git a/go.mod b/go.mod index 883a32b..0e4654e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.5 require ( github.com/alecthomas/kong v1.13.0 github.com/go-sql-driver/mysql v1.9.3 + github.com/google/uuid v1.5.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,10 +13,12 @@ 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 + golang.org/x/crypto v0.46.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.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/fsnotify/fsnotify v1.9.0 // indirect @@ -48,7 +51,6 @@ require ( 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 golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect diff --git a/go.sum b/go.sum index 078406c..41b0634 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,15 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 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= @@ -49,12 +53,15 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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-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/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= 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/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= @@ -128,7 +135,9 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g 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/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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= From b4dbbc47383a2b79f330d301ad88e554cdceba8f Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:55:48 +0900 Subject: [PATCH 05/26] test: add client integration tests --- internal/router/v1/client_test.go | 449 ++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 internal/router/v1/client_test.go diff --git a/internal/router/v1/client_test.go b/internal/router/v1/client_test.go new file mode 100644 index 0000000..bd7975f --- /dev/null +++ b/internal/router/v1/client_test.go @@ -0,0 +1,449 @@ +package v1 + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + _ "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/traPtitech/portal-oidc/internal/repository" + "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) + + handler := NewHandler(clientUseCase) + + 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)) + } +} From 5c83ed18d7989e67f30ba20cae3ae41d7d2e247f Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:53:33 +0900 Subject: [PATCH 06/26] feat: add authorization_codes and tokens tables --- db/query/oidc.sql | 75 +++++++- db/schema.sql | 35 ++++ internal/repository/oidc/db.go | 196 ++++++++++++++++++-- internal/repository/oidc/models.go | 24 +++ internal/repository/oidc/oidc.sql.go | 267 +++++++++++++++++++++++++++ internal/repository/oidc/querier.go | 19 ++ 6 files changed, 597 insertions(+), 19 deletions(-) diff --git a/db/query/oidc.sql b/db/query/oidc.sql index 250a0fe..cd8d621 100644 --- a/db/query/oidc.sql +++ b/db/query/oidc.sql @@ -31,4 +31,77 @@ WHERE client_id = ?; DELETE FROM clients WHERE client_id = ?; -- name: DeleteAllClients :exec -DELETE FROM clients; \ No newline at end of file +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: 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, + 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: DeleteAllTokens :exec +DELETE FROM tokens; \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index 0a40901..b77b445 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -10,3 +10,38 @@ CREATE TABLE IF NOT EXISTS `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, + `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, + `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_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; diff --git a/internal/repository/oidc/db.go b/internal/repository/oidc/db.go index 70cdeb7..7237be1 100644 --- a/internal/repository/oidc/db.go +++ b/internal/repository/oidc/db.go @@ -24,21 +24,69 @@ 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.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.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.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.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.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.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) } @@ -50,31 +98,111 @@ 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.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.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.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.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.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.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) @@ -122,27 +250,59 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar } type Queries struct { - db DBTX - tx *sql.Tx - createClientStmt *sql.Stmt - deleteAllClientsStmt *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 + createTokenStmt *sql.Stmt + deleteAllAuthorizationCodesStmt *sql.Stmt + deleteAllClientsStmt *sql.Stmt + deleteAllTokensStmt *sql.Stmt + deleteAuthorizationCodeStmt *sql.Stmt + deleteClientStmt *sql.Stmt + deleteExpiredAuthorizationCodesStmt *sql.Stmt + deleteExpiredTokensStmt *sql.Stmt + deleteTokenStmt *sql.Stmt + deleteTokenByAccessTokenStmt *sql.Stmt + deleteTokenByRefreshTokenStmt *sql.Stmt + deleteTokensByUserAndClientStmt *sql.Stmt + getAuthorizationCodeStmt *sql.Stmt + getClientStmt *sql.Stmt + getTokenByAccessTokenStmt *sql.Stmt + getTokenByIDStmt *sql.Stmt + getTokenByRefreshTokenStmt *sql.Stmt + listClientsStmt *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, - deleteAllClientsStmt: q.deleteAllClientsStmt, - 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, + createTokenStmt: q.createTokenStmt, + deleteAllAuthorizationCodesStmt: q.deleteAllAuthorizationCodesStmt, + deleteAllClientsStmt: q.deleteAllClientsStmt, + deleteAllTokensStmt: q.deleteAllTokensStmt, + deleteAuthorizationCodeStmt: q.deleteAuthorizationCodeStmt, + deleteClientStmt: q.deleteClientStmt, + deleteExpiredAuthorizationCodesStmt: q.deleteExpiredAuthorizationCodesStmt, + deleteExpiredTokensStmt: q.deleteExpiredTokensStmt, + deleteTokenStmt: q.deleteTokenStmt, + deleteTokenByAccessTokenStmt: q.deleteTokenByAccessTokenStmt, + deleteTokenByRefreshTokenStmt: q.deleteTokenByRefreshTokenStmt, + deleteTokensByUserAndClientStmt: q.deleteTokensByUserAndClientStmt, + getAuthorizationCodeStmt: q.getAuthorizationCodeStmt, + getClientStmt: q.getClientStmt, + getTokenByAccessTokenStmt: q.getTokenByAccessTokenStmt, + getTokenByIDStmt: q.getTokenByIDStmt, + getTokenByRefreshTokenStmt: q.getTokenByRefreshTokenStmt, + listClientsStmt: q.listClientsStmt, + 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..fa82d76 100644 --- a/internal/repository/oidc/models.go +++ b/internal/repository/oidc/models.go @@ -10,6 +10,19 @@ 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"` + 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 +32,14 @@ type Client struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } + +type Token struct { + ID string `json:"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 25ae339..7c8c921 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,52 @@ func (q *Queries) CreateClient(ctx context.Context, arg CreateClientParams) erro return err } +const createToken = `-- name: CreateToken :exec + +INSERT INTO tokens ( + id, + client_id, + user_id, + access_token, + refresh_token, + scopes, + expires_at +) VALUES (?, ?, ?, ?, ?, ?, ?) +` + +type CreateTokenParams struct { + ID string `json:"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.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 ` @@ -51,6 +141,24 @@ func (q *Queries) DeleteAllClients(ctx context.Context) error { 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 = ? ` @@ -60,6 +168,87 @@ 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 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 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, 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.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 = ? ` @@ -79,6 +268,66 @@ func (q *Queries) GetClient(ctx context.Context, clientID string) (Client, error return i, err } +const getTokenByAccessToken = `-- name: GetTokenByAccessToken :one +SELECT 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.ClientID, + &i.UserID, + &i.AccessToken, + &i.RefreshToken, + &i.Scopes, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} + +const getTokenByID = `-- name: GetTokenByID :one +SELECT 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.ClientID, + &i.UserID, + &i.AccessToken, + &i.RefreshToken, + &i.Scopes, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} + +const getTokenByRefreshToken = `-- name: GetTokenByRefreshToken :one +SELECT 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.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 ` @@ -114,6 +363,24 @@ func (q *Queries) ListClients(ctx context.Context) ([]Client, error) { return items, nil } +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 d4a54ea..33b1031 100644 --- a/internal/repository/oidc/querier.go +++ b/internal/repository/oidc/querier.go @@ -6,15 +6,34 @@ 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 + // Token queries + CreateToken(ctx context.Context, arg CreateTokenParams) error + DeleteAllAuthorizationCodes(ctx context.Context) error DeleteAllClients(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 + DeleteToken(ctx context.Context, id string) error + DeleteTokenByAccessToken(ctx context.Context, accessToken string) error + DeleteTokenByRefreshToken(ctx context.Context, refreshToken sql.NullString) error + DeleteTokensByUserAndClient(ctx context.Context, arg DeleteTokensByUserAndClientParams) error + GetAuthorizationCode(ctx context.Context, code string) (AuthorizationCode, error) GetClient(ctx context.Context, clientID string) (Client, 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) + UpdateAuthorizationCodePKCE(ctx context.Context, arg UpdateAuthorizationCodePKCEParams) error UpdateClient(ctx context.Context, arg UpdateClientParams) error UpdateClientSecret(ctx context.Context, arg UpdateClientSecretParams) error } From 4d7b89e8a096550bc8485b2a2b59204cc09526e6 Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:53:38 +0900 Subject: [PATCH 07/26] feat: add OAuth2/OIDC API definition and generated code --- api/openapi.yaml | 363 +++++++++++++++++++++++++ internal/router/v1/gen/models.go | 188 +++++++++++++ internal/router/v1/gen/server.go | 448 +++++++++++++++++++++++++++++++ 3 files changed, 999 insertions(+) diff --git a/api/openapi.yaml b/api/openapi.yaml index a41b074..193bdc6 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -140,6 +140,173 @@ paths: "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: ClientType: @@ -238,3 +405,199 @@ components: 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 + bearerFormat: JWT diff --git a/internal/router/v1/gen/models.go b/internal/router/v1/gen/models.go index e9b0aff..d0ba90d 100644 --- a/internal/router/v1/gen/models.go +++ b/internal/router/v1/gen/models.go @@ -9,12 +9,56 @@ import ( 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"` @@ -72,8 +116,152 @@ type ClientWithSecret struct { 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 436a600..4070b95 100644 --- a/internal/router/v1/gen/server.go +++ b/internal/router/v1/gen/server.go @@ -17,6 +17,12 @@ import ( // 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 @@ -35,6 +41,18 @@ type ServerInterface interface { // クライアントシークレット再生成 // (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. @@ -42,6 +60,24 @@ 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 @@ -124,6 +160,104 @@ func (w *ServerInterfaceWrapper) RegenerateClientSecret(ctx echo.Context) error 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 @@ -152,13 +286,51 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL 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 { @@ -358,8 +530,122 @@ func (response RegenerateClientSecret404Response) VisitRegenerateClientSecretRes 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) @@ -378,6 +664,18 @@ type StrictServerInterface interface { // クライアントシークレット再生成 // (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 @@ -392,6 +690,52 @@ type strictHandler struct { 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 @@ -549,3 +893,107 @@ func (sh *strictHandler) RegenerateClientSecret(ctx echo.Context, clientId opena } 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 +} From aa4c5dfbb67dce41cb8d9c2357ccd38a8e26e0f3 Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:53:41 +0900 Subject: [PATCH 08/26] feat: implement OAuth2/OIDC endpoints --- cmd/oauth.go | 191 ++++++++++ internal/repository/oauth_storage.go | 444 +++++++++++++++++++++++ internal/router/v1/auth.go | 114 ++++++ internal/router/v1/handler.go | 38 +- internal/router/v1/handler_test.go | 510 +++++++++++++++++++++++++++ internal/router/v1/oauth.go | 166 +++++++++ 6 files changed, 1461 insertions(+), 2 deletions(-) create mode 100644 cmd/oauth.go create mode 100644 internal/repository/oauth_storage.go create mode 100644 internal/router/v1/auth.go create mode 100644 internal/router/v1/handler_test.go create mode 100644 internal/router/v1/oauth.go diff --git a/cmd/oauth.go b/cmd/oauth.go new file mode 100644 index 0000000..e25bee7 --- /dev/null +++ b/cmd/oauth.go @@ -0,0 +1,191 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "io" + "os" + "path/filepath" + "time" + + "github.com/ory/fosite" + "github.com/ory/fosite/compose" + "github.com/ory/fosite/token/jwt" + + "github.com/traPtitech/portal-oidc/internal/repository" +) + +type OAuthProviderConfig struct { + Issuer string + AccessTokenLifespan time.Duration + RefreshTokenLifespan time.Duration + AuthCodeLifespan time.Duration + IDTokenLifespan time.Duration + Secret []byte +} + +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 *repository.OAuthStorage, 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: false, + 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") + } + + return x509.ParsePKCS1PrivateKey(block.Bytes) +} + +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/internal/repository/oauth_storage.go b/internal/repository/oauth_storage.go new file mode 100644 index 0000000..73093e0 --- /dev/null +++ b/internal/repository/oauth_storage.go @@ -0,0 +1,444 @@ +package repository + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/token/jwt" + "golang.org/x/crypto/bcrypt" + + "github.com/traPtitech/portal-oidc/internal/repository/oidc" +) + +// OAuthStorage implements fosite.Storage interface +type OAuthStorage struct { + queries *oidc.Queries + oidcSessions map[string]fosite.Requester + oidcSessionsMutex sync.RWMutex +} + +func NewOAuthStorage(queries *oidc.Queries) *OAuthStorage { + return &OAuthStorage{ + queries: queries, + oidcSessions: make(map[string]fosite.Requester), + } +} + +// ClientCredentials implements fosite.ClientCredentialsStorage +func (s *OAuthStorage) GetClient(ctx context.Context, id string) (fosite.Client, error) { + dbClient, err := s.queries.GetClient(ctx, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + var redirectURIs []string + if err := json.Unmarshal(dbClient.RedirectUris, &redirectURIs); err != nil { + return nil, err + } + + return &OAuthClient{ + ID: dbClient.ClientID, + Secret: []byte(dbClient.ClientSecretHash.String), + RedirectURIs: redirectURIs, + GrantTypes: []string{"authorization_code", "refresh_token"}, + ResponseTypes: []string{"code"}, + Scopes: []string{"openid", "profile", "email"}, + Public: dbClient.ClientType == "public", + }, nil +} + +func (s *OAuthStorage) ClientAssertionJWTValid(ctx context.Context, jti string) error { + return fosite.ErrNotFound +} + +func (s *OAuthStorage) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error { + return nil +} + +// AuthorizeCodeStorage implements fosite.AuthorizeCodeStorage +func (s *OAuthStorage) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) error { + sess, ok := request.GetSession().(*OAuthSession) + if !ok { + return errors.New("invalid session type") + } + + return s.queries.CreateAuthorizationCode(ctx, oidc.CreateAuthorizationCodeParams{ + Code: code, + ClientID: request.GetClient().GetID(), + UserID: sess.Subject, + RedirectUri: request.GetRequestForm().Get("redirect_uri"), + Scopes: strings.Join(request.GetRequestedScopes(), " "), + CodeChallenge: sql.NullString{Valid: false}, + CodeChallengeMethod: sql.NullString{Valid: false}, + Nonce: sql.NullString{ + String: request.GetRequestForm().Get("nonce"), + Valid: request.GetRequestForm().Get("nonce") != "", + }, + ExpiresAt: sess.ExpiresAt[fosite.AuthorizeCode], + }) +} + +func (s *OAuthStorage) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) { + dbCode, err := s.queries.GetAuthorizationCode(ctx, code) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + if time.Now().After(dbCode.ExpiresAt) { + return nil, fosite.ErrTokenExpired + } + + client, err := s.GetClient(ctx, dbCode.ClientID) + if err != nil { + return nil, err + } + + scopes := strings.Split(dbCode.Scopes, " ") + if dbCode.Scopes == "" { + scopes = []string{} + } + + sess := NewOAuthSession(dbCode.UserID) + sess.ExpiresAt[fosite.AuthorizeCode] = dbCode.ExpiresAt + + form := make(map[string][]string) + form["redirect_uri"] = []string{dbCode.RedirectUri} + if dbCode.CodeChallenge.Valid { + form["code_challenge"] = []string{dbCode.CodeChallenge.String} + } + if dbCode.CodeChallengeMethod.Valid { + form["code_challenge_method"] = []string{dbCode.CodeChallengeMethod.String} + } + if dbCode.Nonce.Valid { + form["nonce"] = []string{dbCode.Nonce.String} + } + + req := &fosite.Request{ + ID: code, + RequestedAt: dbCode.CreatedAt, + Client: client, + Form: form, + Session: sess, + } + req.SetRequestedScopes(scopes) + for _, scope := range scopes { + req.GrantScope(scope) + } + return req, nil +} + +func (s *OAuthStorage) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error { + return s.queries.DeleteAuthorizationCode(ctx, code) +} + +// PKCERequestStorage implements fosite.PKCERequestStorage +func (s *OAuthStorage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + return s.GetAuthorizeCodeSession(ctx, signature, session) +} + +func (s *OAuthStorage) 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.queries.UpdateAuthorizationCodePKCE(ctx, oidc.UpdateAuthorizationCodePKCEParams{ + CodeChallenge: sql.NullString{ + String: challenge, + Valid: true, + }, + CodeChallengeMethod: sql.NullString{ + String: method, + Valid: method != "", + }, + Code: signature, + }) +} + +func (s *OAuthStorage) DeletePKCERequestSession(ctx context.Context, signature string) error { + return nil +} + +// AccessTokenStorage implements fosite.AccessTokenStorage +func (s *OAuthStorage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error { + sess, ok := request.GetSession().(*OAuthSession) + if !ok { + return errors.New("invalid session type") + } + + tokenID := uuid.New() + return s.queries.CreateToken(ctx, oidc.CreateTokenParams{ + ID: tokenID.String(), + ClientID: request.GetClient().GetID(), + UserID: sess.Subject, + AccessToken: signature, + RefreshToken: sql.NullString{ + Valid: false, + }, + Scopes: strings.Join(request.GetGrantedScopes(), " "), + ExpiresAt: sess.ExpiresAt[fosite.AccessToken], + }) +} + +func (s *OAuthStorage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + dbToken, err := s.queries.GetTokenByAccessToken(ctx, signature) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + if time.Now().After(dbToken.ExpiresAt) { + return nil, fosite.ErrTokenExpired + } + + client, err := s.GetClient(ctx, dbToken.ClientID) + if err != nil { + return nil, err + } + + scopes := strings.Split(dbToken.Scopes, " ") + if dbToken.Scopes == "" { + scopes = []string{} + } + + sess := NewOAuthSession(dbToken.UserID) + sess.ExpiresAt[fosite.AccessToken] = dbToken.ExpiresAt + + req := &fosite.Request{ + ID: dbToken.ID, + RequestedAt: dbToken.CreatedAt, + Client: client, + Session: sess, + } + req.SetRequestedScopes(scopes) + for _, scope := range scopes { + req.GrantScope(scope) + } + return req, nil +} + +func (s *OAuthStorage) DeleteAccessTokenSession(ctx context.Context, signature string) error { + return s.queries.DeleteTokenByAccessToken(ctx, signature) +} + +// RefreshTokenStorage implements fosite.RefreshTokenStorage +func (s *OAuthStorage) CreateRefreshTokenSession(ctx context.Context, signature string, accessSignature string, request fosite.Requester) error { + sess, ok := request.GetSession().(*OAuthSession) + if !ok { + return errors.New("invalid session type") + } + + tokenID := uuid.New() + return s.queries.CreateToken(ctx, oidc.CreateTokenParams{ + ID: tokenID.String(), + ClientID: request.GetClient().GetID(), + UserID: sess.Subject, + AccessToken: accessSignature, + RefreshToken: sql.NullString{ + String: signature, + Valid: true, + }, + Scopes: strings.Join(request.GetGrantedScopes(), " "), + ExpiresAt: sess.ExpiresAt[fosite.RefreshToken], + }) +} + +func (s *OAuthStorage) RotateRefreshToken(ctx context.Context, requestID string, refreshTokenSignature string) error { + return nil +} + +func (s *OAuthStorage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + dbToken, err := s.queries.GetTokenByRefreshToken(ctx, sql.NullString{String: signature, Valid: true}) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fosite.ErrNotFound + } + return nil, err + } + + client, err := s.GetClient(ctx, dbToken.ClientID) + if err != nil { + return nil, err + } + + scopes := strings.Split(dbToken.Scopes, " ") + if dbToken.Scopes == "" { + scopes = []string{} + } + + sess := NewOAuthSession(dbToken.UserID) + sess.ExpiresAt[fosite.RefreshToken] = dbToken.ExpiresAt + + req := &fosite.Request{ + ID: dbToken.ID, + RequestedAt: dbToken.CreatedAt, + Client: client, + Session: sess, + } + req.SetRequestedScopes(scopes) + for _, scope := range scopes { + req.GrantScope(scope) + } + return req, nil +} + +func (s *OAuthStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) error { + return s.queries.DeleteTokenByRefreshToken(ctx, sql.NullString{String: signature, Valid: true}) +} + +func (s *OAuthStorage) RevokeRefreshToken(ctx context.Context, requestID string) error { + return nil +} + +func (s *OAuthStorage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, signature string) error { + return s.DeleteRefreshTokenSession(ctx, signature) +} + +func (s *OAuthStorage) RevokeAccessToken(ctx context.Context, requestID string) error { + return nil +} + +// OpenIDConnectRequestStorage implementation + +func (s *OAuthStorage) CreateOpenIDConnectSession(_ context.Context, authorizeCode string, requester fosite.Requester) error { + s.oidcSessionsMutex.Lock() + defer s.oidcSessionsMutex.Unlock() + s.oidcSessions[authorizeCode] = requester + return nil +} + +func (s *OAuthStorage) GetOpenIDConnectSession(_ context.Context, authorizeCode string, _ fosite.Requester) (fosite.Requester, error) { + s.oidcSessionsMutex.RLock() + defer s.oidcSessionsMutex.RUnlock() + req, ok := s.oidcSessions[authorizeCode] + if !ok { + return nil, fosite.ErrNotFound + } + return req, nil +} + +func (s *OAuthStorage) DeleteOpenIDConnectSession(_ context.Context, authorizeCode string) error { + s.oidcSessionsMutex.Lock() + defer s.oidcSessionsMutex.Unlock() + delete(s.oidcSessions, authorizeCode) + return nil +} + +// OAuthClient implements fosite.Client +type OAuthClient struct { + ID string + Secret []byte + RedirectURIs []string + GrantTypes []string + ResponseTypes []string + Scopes []string + Public bool +} + +func (c *OAuthClient) GetID() string { return c.ID } +func (c *OAuthClient) GetHashedSecret() []byte { return c.Secret } +func (c *OAuthClient) GetRedirectURIs() []string { return c.RedirectURIs } +func (c *OAuthClient) GetGrantTypes() fosite.Arguments { return c.GrantTypes } +func (c *OAuthClient) GetResponseTypes() fosite.Arguments { + if len(c.ResponseTypes) == 0 { + return []string{"code"} + } + return c.ResponseTypes +} +func (c *OAuthClient) GetScopes() fosite.Arguments { return c.Scopes } +func (c *OAuthClient) IsPublic() bool { return c.Public } +func (c *OAuthClient) GetAudience() fosite.Arguments { return nil } + +// OAuthSession implements fosite.Session and openid.Session +type OAuthSession struct { + Subject string + Username string + ExpiresAt map[fosite.TokenType]time.Time + Extra map[string]interface{} + idTokenClaims *jwt.IDTokenClaims + idTokenHeaders *jwt.Headers +} + +var _ openid.Session = (*OAuthSession)(nil) + +func NewOAuthSession(subject string) *OAuthSession { + return &OAuthSession{ + Subject: subject, + Username: subject, + ExpiresAt: make(map[fosite.TokenType]time.Time), + Extra: make(map[string]interface{}), + idTokenClaims: &jwt.IDTokenClaims{ + Subject: subject, + }, + idTokenHeaders: &jwt.Headers{ + Extra: make(map[string]interface{}), + }, + } +} + +func (s *OAuthSession) 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 *OAuthSession) GetExpiresAt(key fosite.TokenType) time.Time { + if s.ExpiresAt == nil { + return time.Time{} + } + return s.ExpiresAt[key] +} + +func (s *OAuthSession) GetUsername() string { return s.Username } +func (s *OAuthSession) GetSubject() string { return s.Subject } +func (s *OAuthSession) IDTokenClaims() *jwt.IDTokenClaims { return s.idTokenClaims } +func (s *OAuthSession) IDTokenHeaders() *jwt.Headers { return s.idTokenHeaders } + +func (s *OAuthSession) Clone() fosite.Session { + expiresAt := make(map[fosite.TokenType]time.Time) + for k, v := range s.ExpiresAt { + expiresAt[k] = v + } + extra := make(map[string]interface{}) + for k, v := range s.Extra { + extra[k] = v + } + idTokenClaimsClone := *s.idTokenClaims + idTokenHeadersClone := *s.idTokenHeaders + idTokenHeadersClone.Extra = make(map[string]interface{}) + for k, v := range s.idTokenHeaders.Extra { + idTokenHeadersClone.Extra[k] = v + } + return &OAuthSession{ + Subject: s.Subject, + Username: s.Username, + ExpiresAt: expiresAt, + Extra: extra, + idTokenClaims: &idTokenClaimsClone, + idTokenHeaders: &idTokenHeadersClone, + } +} + +// ValidateClientSecret validates the client secret using bcrypt +func ValidateClientSecret(hashedSecret []byte, secret string) bool { + return bcrypt.CompareHashAndPassword(hashedSecret, []byte(secret)) == nil +} diff --git a/internal/router/v1/auth.go b/internal/router/v1/auth.go new file mode 100644 index 0000000..7f750df --- /dev/null +++ b/internal/router/v1/auth.go @@ -0,0 +1,114 @@ +package v1 + +import ( + "html" + "net/http" + + "github.com/labstack/echo/v4" +) + +const sessionName = "oidc_session" + +func (h *Handler) GetLogin(ctx echo.Context) error { + returnURL := ctx.QueryParam("return_url") + if returnURL == "" { + returnURL = "/" + } + + page := ` + +
+Test user: testuser / password
+ +` + + 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") + + if username != "testuser" || password != "password" { + return ctx.HTML(http.StatusUnauthorized, ` + +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"] = username + session.Values["authenticated"] = true + + if err := session.Save(ctx.Request(), ctx.Response()); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to save session") + } + + if returnURL == "" { + returnURL = "/" + } + return ctx.Redirect(http.StatusFound, returnURL) +} + +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 (h *Handler) getAuthenticatedUser(ctx echo.Context) string { + session, err := h.sessions.Get(ctx.Request(), sessionName) + if err != nil { + return "" + } + + authenticated, ok := session.Values["authenticated"].(bool) + if !ok || !authenticated { + return "" + } + + userID, ok := session.Values["user_id"].(string) + if !ok { + return "" + } + + return userID +} diff --git a/internal/router/v1/handler.go b/internal/router/v1/handler.go index 6386d44..da22374 100644 --- a/internal/router/v1/handler.go +++ b/internal/router/v1/handler.go @@ -1,13 +1,47 @@ package v1 -import "github.com/traPtitech/portal-oidc/internal/usecase" +import ( + "crypto/rsa" + "net/http" + + "github.com/gorilla/sessions" + "github.com/ory/fosite" + + "github.com/traPtitech/portal-oidc/internal/usecase" +) type Handler struct { clientUseCase usecase.ClientUseCase + oauth2 fosite.OAuth2Provider + sessions *sessions.CookieStore + config OAuthConfig +} + +type OAuthConfig struct { + Issuer string + SessionSecret []byte + PrivateKey *rsa.PrivateKey + Environment string + TestUserID string } -func NewHandler(clientUseCase usecase.ClientUseCase) *Handler { +func NewHandler( + clientUseCase usecase.ClientUseCase, + oauth2 fosite.OAuth2Provider, + config OAuthConfig, +) *Handler { + store := sessions.NewCookieStore(config.SessionSecret) + store.Options = &sessions.Options{ + Path: "/", + MaxAge: 3600, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + } + return &Handler{ clientUseCase: clientUseCase, + oauth2: oauth2, + sessions: store, + config: config, } } diff --git a/internal/router/v1/handler_test.go b/internal/router/v1/handler_test.go new file mode 100644 index 0000000..2e05fec --- /dev/null +++ b/internal/router/v1/handler_test.go @@ -0,0 +1,510 @@ +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/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) + + // 環境変数で上書き (MARIADB_USERNAME など) + _ = 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") + + // Connect without database to create test database + 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) + } + + // Create test database + _, 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() + + // Connect to test database + 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) + } + + // Load and execute schema + 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) + } + + // Run tests + code := m.Run() + + // Cleanup + _ = 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) + } + + // Clean up tables before test + if err := queries.DeleteAllClients(ctx); err != nil { + t.Fatalf("failed to clean up clients table: %v", err) + } + if err := queries.DeleteAllAuthorizationCodes(ctx); err != nil { + t.Fatalf("failed to clean up authorization_codes table: %v", err) + } + if err := queries.DeleteAllTokens(ctx); err != nil { + t.Fatalf("failed to clean up tokens table: %v", err) + } + + clientRepo := repository.NewClientRepository(queries) + clientUseCase := usecase.NewClientUseCase(clientRepo) + + // Create OAuth2 provider for testing + oauthStorage := repository.NewOAuthStorage(queries) + fositeConfig := &fosite.Config{ + 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, OAuthConfig{ + Issuer: "http://localhost:8080", + }) + + cleanup := func() { + _ = queries.DeleteAllTokens(ctx) + _ = queries.DeleteAllAuthorizationCodes(ctx) + _ = 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) + + // Create a client first + 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) + + // Get clients list + 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) + + // Create a client + 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) + } + + // Get client by ID + 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) + + // Create a client + 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) + } + + // Update client + 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) + + // Create a client + 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) + } + + // 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("status = %d, want %d", rec.Code, http.StatusNoContent) + } + + // Verify deletion + 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) + + // Create a client + 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) + } + + // 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("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/oauth.go b/internal/router/v1/oauth.go new file mode 100644 index 0000000..1fa3cd8 --- /dev/null +++ b/internal/router/v1/oauth.go @@ -0,0 +1,166 @@ +package v1 + +import ( + "crypto/sha256" + "encoding/base64" + "errors" + "net/http" + "net/url" + "strings" + + "github.com/go-jose/go-jose/v4" + "github.com/labstack/echo/v4" + "github.com/ory/fosite" + + "github.com/traPtitech/portal-oidc/internal/repository" + "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() + + var userID string + if h.config.Environment != "production" { + userID = h.config.TestUserID + } else { + userID = h.getAuthenticatedUser(ctx) + if userID == "" { + returnURL := req.URL.String() + return ctx.Redirect(http.StatusFound, "/login?return_url="+url.QueryEscape(returnURL)) + } + } + + ar, err := h.oauth2.NewAuthorizeRequest(c, req) + if err != nil { + h.oauth2.WriteAuthorizeError(c, rw, ar, err) + return nil + } + + session := repository.NewOAuthSession(userID) + 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) Token(ctx echo.Context) error { + c := ctx.Request().Context() + rw := ctx.Response() + req := ctx.Request() + + session := repository.NewOAuthSession("") + 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, repository.NewOAuthSession("")) + if err != nil { + return ctx.JSON(http.StatusUnauthorized, gen.OAuthError{Error: gen.InvalidGrant}) + } + + return ctx.JSON(http.StatusOK, gen.UserInfo{Sub: ar.GetSession().GetSubject()}) +} + +func (h *Handler) GetJWKS(ctx echo.Context) error { + 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 := 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, + }) +} From aac184de3ae3ff3fba8128b5c30ec02b62b0b573 Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:53:45 +0900 Subject: [PATCH 09/26] feat: integrate OAuth2/OIDC into server --- cmd/config.go | 3 +- cmd/serve.go | 39 ++- config.yaml | 4 + go.mod | 99 ++++-- go.sum | 848 ++++++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 862 insertions(+), 131 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index e7d1efa..ad428e9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -66,7 +66,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/serve.go b/cmd/serve.go index dd278de..127372f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -23,23 +23,44 @@ func newServer(cfg Config) (http.Handler, error) { return nil, err } - clientRepo := repository.NewClientRepository(queries) - clientUC := usecase.NewClientUseCase(clientRepo) - handler := v1.NewHandler(clientUC) + privateKey, err := loadOrGenerateKey(cfg.OAuth.KeyFile) + if err != nil { + return nil, fmt.Errorf("failed to load/generate RSA key: %w", err) + } + + oauthStorage := repository.NewOAuthStorage(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) + + handler := v1.NewHandler( + usecase.NewClientUseCase(repository.NewClientRepository(queries)), + oauth2Provider, + 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()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, AllowHeaders: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, })) - gen.RegisterHandlers(e, handler) - - e.GET("/health", func(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) - }) + e.GET("/login", handler.GetLogin) + e.POST("/login", handler.PostLogin) + e.GET("/logout", handler.Logout) return e, nil } diff --git a/config.yaml b/config.yaml index 9a3da1a..22c163b 100644 --- a/config.yaml +++ b/config.yaml @@ -7,3 +7,7 @@ database: user: root password: password name: oidc + +oauth: + test_mode: true + test_user_id: testuser diff --git a/go.mod b/go.mod index 0e4654e..98b5551 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.25.5 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.5.0 + 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 @@ -13,54 +15,95 @@ 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 - golang.org/x/crypto v0.46.0 + github.com/ory/fosite v0.49.0 + golang.org/x/crypto v0.47.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.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/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.3.0 // 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/exp v0.0.0-20250620022241-b7579e27df2b // 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.21.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.21.0 // indirect + go.opentelemetry.io/otel/sdk v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.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 41b0634..ace8efe 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,46 @@ +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= @@ -9,61 +50,243 @@ 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.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/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.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/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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= @@ -72,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= @@ -85,158 +314,591 @@ 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/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/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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +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/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.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +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.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +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/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= From b00091212536602259efaadb1cb7fc3298528bcf Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:03:22 +0900 Subject: [PATCH 10/26] fix: restore Recover middleware and health endpoint --- cmd/serve.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/serve.go b/cmd/serve.go index 127372f..0b235e9 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -52,6 +52,7 @@ func newServer(cfg Config) (http.Handler, error) { ) e := echo.New() + e.Use(middleware.Recover()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, AllowHeaders: []string{"*"}, @@ -61,6 +62,9 @@ func newServer(cfg Config) (http.Handler, error) { 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"}) + }) return e, nil } From 7e6a48d0c83c8c4e905cdbe68af986e1ef7c0ca2 Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:37:45 +0900 Subject: [PATCH 11/26] chore: remove unused test_mode from config --- config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/config.yaml b/config.yaml index 22c163b..3b29137 100644 --- a/config.yaml +++ b/config.yaml @@ -9,5 +9,4 @@ database: name: oidc oauth: - test_mode: true test_user_id: testuser From dd0d6c59eeb0a228ffd5ea101bb521652dea6d7d Mon Sep 17 00:00:00 2001 From: anko9801 <37263451+anko9801@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:59:43 +0900 Subject: [PATCH 12/26] feat: add Portal authentication integration --- cmd/config.go | 30 +- cmd/serve.go | 38 ++- config.yaml | 8 + internal/repository/user.go | 134 ++++++++ internal/router/v1/auth.go | 46 ++- internal/router/v1/client_test.go | 36 +- internal/router/v1/handler.go | 4 + internal/router/v1/handler_test.go | 510 ----------------------------- 8 files changed, 276 insertions(+), 530 deletions(-) create mode 100644 internal/repository/user.go delete mode 100644 internal/router/v1/handler_test.go diff --git a/cmd/config.go b/cmd/config.go index ad428e9..1669e6b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -15,9 +15,14 @@ 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"` @@ -33,16 +38,21 @@ type OAuthConfig struct { } 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) { diff --git a/cmd/serve.go b/cmd/serve.go index 0b235e9..733c2e5 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -12,13 +12,19 @@ import ( "github.com/traPtitech/portal-oidc/internal/repository" "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" ) func newServer(cfg Config) (http.Handler, error) { - queries, err := setupDatabase(cfg.Database) + queries, err := setupOIDCDatabase(cfg.Database) + if err != nil { + return nil, err + } + + portalQueries, err := setupPortalDatabase(cfg.Portal.Database) if err != nil { return nil, err } @@ -42,6 +48,7 @@ func newServer(cfg Config) (http.Handler, error) { handler := v1.NewHandler( usecase.NewClientUseCase(repository.NewClientRepository(queries)), oauth2Provider, + repository.NewUserRepository(portalQueries), v1.OAuthConfig{ Issuer: cfg.Host, SessionSecret: []byte(cfg.OAuth.Secret), @@ -69,22 +76,43 @@ func newServer(cfg Config) (http.Handler, error) { return e, nil } -func setupDatabase(cfg DatabaseConfig) (*oidc.Queries, error) { +func setupOIDCDatabase(cfg DatabaseConfig) (*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, 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, 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, fmt.Errorf("failed to prepare oidc queries: %w", err) + } + + return 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/config.yaml b/config.yaml index 3b29137..c304362 100644 --- a/config.yaml +++ b/config.yaml @@ -8,5 +8,13 @@ database: password: password name: oidc +portal: + database: + host: localhost + port: 3306 + user: root + password: password + name: portal + oauth: test_user_id: testuser diff --git a/internal/repository/user.go b/internal/repository/user.go new file mode 100644 index 0000000..459407d --- /dev/null +++ b/internal/repository/user.go @@ -0,0 +1,134 @@ +package repository + +import ( + "context" + "crypto/sha512" + "crypto/subtle" + "database/sql" + "encoding/base64" + "errors" + "strings" + + "golang.org/x/crypto/pbkdf2" + + "github.com/traPtitech/portal-oidc/internal/repository/portal" +) + +var ( + ErrUserNotFound = errors.New("user not found") + ErrInvalidPassword = errors.New("invalid password") + ErrUserNotActive = errors.New("user is not active") +) + +type User struct { + ID string + TrapID string +} + +type UserRepository interface { + Authenticate(ctx context.Context, trapID, password string) (*User, error) + GetByID(ctx context.Context, id string) (*User, error) +} + +type userRepository struct { + queries *portal.Queries +} + +func NewUserRepository(queries *portal.Queries) UserRepository { + return &userRepository{queries: queries} +} + +func (r *userRepository) Authenticate(ctx context.Context, trapID, password string) (*User, error) { + user, err := r.queries.GetUserByTrapID(ctx, trapID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } + return nil, err + } + + if !verifyPBKDF2Password(password, user.PasswordHash) { + return nil, ErrInvalidPassword + } + + statuses, err := r.queries.ListUserStatuses(ctx, user.ID) + if err != nil { + return nil, err + } + + isActive := false + for _, status := range statuses { + if status.Status == "active" { + isActive = true + break + } + } + + if !isActive && len(statuses) > 0 { + return nil, ErrUserNotActive + } + + return &User{ + ID: user.ID, + TrapID: user.TrapID, + }, nil +} + +func (r *userRepository) GetByID(ctx context.Context, id string) (*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 &User{ + ID: user.ID, + TrapID: user.TrapID, + }, nil +} + +// verifyPBKDF2Password verifies a password against a PBKDF2-SHA512 hash. +// Expected format: "pbkdf2_sha512$iterations$salt$hash" (base64 encoded) +func verifyPBKDF2Password(password, storedHash string) bool { + parts := strings.Split(storedHash, "$") + if len(parts) != 4 { + return false + } + + algorithm := parts[0] + if algorithm != "pbkdf2_sha512" { + return false + } + + iterations, err := parseIterations(parts[1]) + if err != nil { + return false + } + + salt, err := base64.StdEncoding.DecodeString(parts[2]) + if err != nil { + return false + } + + expectedHash, err := base64.StdEncoding.DecodeString(parts[3]) + if err != nil { + return false + } + + computedHash := pbkdf2.Key([]byte(password), salt, iterations, len(expectedHash), sha512.New) + + return subtle.ConstantTimeCompare(computedHash, expectedHash) == 1 +} + +func parseIterations(s string) (int, error) { + var n int + for _, c := range s { + if c < '0' || c > '9' { + return 0, errors.New("invalid iterations") + } + n = n*10 + int(c-'0') + } + return n, nil +} diff --git a/internal/router/v1/auth.go b/internal/router/v1/auth.go index 7f750df..5574c68 100644 --- a/internal/router/v1/auth.go +++ b/internal/router/v1/auth.go @@ -1,10 +1,13 @@ package v1 import ( + "errors" "html" "net/http" "github.com/labstack/echo/v4" + + "github.com/traPtitech/portal-oidc/internal/repository" ) const sessionName = "oidc_session" @@ -15,6 +18,11 @@ func (h *Handler) GetLogin(ctx echo.Context) error { returnURL = "/" } + devNote := "" + if h.config.Environment != "production" { + devNote = `Test user: testuser / password
` + } + page := ` @@ -31,11 +39,11 @@ func (h *Handler) GetLogin(ctx echo.Context) error {Test user: testuser / password
+ ` + devNote + ` ` @@ -47,7 +55,16 @@ func (h *Handler) PostLogin(ctx echo.Context) error { password := ctx.FormValue("password") returnURL := ctx.FormValue("return_url") - if username != "testuser" || password != "password" { + 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, `