From e67f0d7e5a89924d232098b75f51ba9c1c11477e Mon Sep 17 00:00:00 2001 From: nihonium Date: Sat, 6 Dec 2025 04:03:04 +0300 Subject: [PATCH] feat: get impersonation token implementation --- auth/auth.gen.go | 115 +++++++++++++++++++++++++-- auth/openapi-auth.yaml | 124 +++++++++++------------------- modules/auth/handlers/handlers.go | 38 ++++++++- modules/auth/queries.sql | 4 + sql/migrations/000001_init.up.sql | 5 +- sql/models.go | 5 +- sql/queries.sql.go | 13 ++++ 7 files changed, 209 insertions(+), 95 deletions(-) diff --git a/auth/auth.gen.go b/auth/auth.gen.go index b7cd839..89a2168 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -13,6 +13,23 @@ import ( strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" ) +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// GetImpersonationTokenJSONBody defines parameters for GetImpersonationToken. +type GetImpersonationTokenJSONBody struct { + TgId *int64 `json:"tg_id,omitempty"` + UserId *int64 `json:"user_id,omitempty"` + union json.RawMessage +} + +// GetImpersonationTokenJSONBody0 defines parameters for GetImpersonationToken. +type GetImpersonationTokenJSONBody0 = interface{} + +// GetImpersonationTokenJSONBody1 defines parameters for GetImpersonationToken. +type GetImpersonationTokenJSONBody1 = interface{} + // PostSignInJSONBody defines parameters for PostSignIn. type PostSignInJSONBody struct { Nickname string `json:"nickname"` @@ -25,6 +42,9 @@ type PostSignUpJSONBody struct { Pass string `json:"pass"` } +// GetImpersonationTokenJSONRequestBody defines body for GetImpersonationToken for application/json ContentType. +type GetImpersonationTokenJSONRequestBody GetImpersonationTokenJSONBody + // PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType. type PostSignInJSONRequestBody PostSignInJSONBody @@ -33,6 +53,9 @@ type PostSignUpJSONRequestBody PostSignUpJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { + // Get service impersontaion token + // (POST /get-impersonation-token) + GetImpersonationToken(c *gin.Context) // Sign in a user and return JWT // (POST /sign-in) PostSignIn(c *gin.Context) @@ -50,6 +73,21 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) +// GetImpersonationToken operation middleware +func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) { + + c.Set(BearerAuthScopes, []string{}) + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetImpersonationToken(c) +} + // PostSignIn operation middleware func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { @@ -103,10 +141,41 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } + router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken) router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn) router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp) } +type UnauthorizedErrorResponse struct { +} + +type GetImpersonationTokenRequestObject struct { + Body *GetImpersonationTokenJSONRequestBody +} + +type GetImpersonationTokenResponseObject interface { + VisitGetImpersonationTokenResponse(w http.ResponseWriter) error +} + +type GetImpersonationToken200JSONResponse struct { + // AccessToken JWT access token + AccessToken string `json:"access_token"` +} + +func (response GetImpersonationToken200JSONResponse) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetImpersonationToken401Response = UnauthorizedErrorResponse + +func (response GetImpersonationToken401Response) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error { + w.WriteHeader(401) + return nil +} + type PostSignInRequestObject struct { Body *PostSignInJSONRequestBody } @@ -127,15 +196,11 @@ func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.Respons return json.NewEncoder(w).Encode(response) } -type PostSignIn401JSONResponse struct { - Error *string `json:"error,omitempty"` -} +type PostSignIn401Response = UnauthorizedErrorResponse -func (response PostSignIn401JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") +func (response PostSignIn401Response) VisitPostSignInResponse(w http.ResponseWriter) error { w.WriteHeader(401) - - return json.NewEncoder(w).Encode(response) + return nil } type PostSignUpRequestObject struct { @@ -159,6 +224,9 @@ func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.Respons // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // Get service impersontaion token + // (POST /get-impersonation-token) + GetImpersonationToken(ctx context.Context, request GetImpersonationTokenRequestObject) (GetImpersonationTokenResponseObject, error) // Sign in a user and return JWT // (POST /sign-in) PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error) @@ -179,6 +247,39 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } +// GetImpersonationToken operation middleware +func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) { + var request GetImpersonationTokenRequestObject + + var body GetImpersonationTokenJSONRequestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + ctx.Status(http.StatusBadRequest) + ctx.Error(err) + return + } + request.Body = &body + + handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetImpersonationToken(ctx, request.(GetImpersonationTokenRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetImpersonationToken") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.Error(err) + ctx.Status(http.StatusInternalServerError) + } else if validResponse, ok := response.(GetImpersonationTokenResponseObject); ok { + if err := validResponse.VisitGetImpersonationTokenResponse(ctx.Writer); err != nil { + ctx.Error(err) + } + } else if response != nil { + ctx.Error(fmt.Errorf("unexpected response type: %T", response)) + } +} + // PostSignIn operation middleware func (sh *strictHandler) PostSignIn(ctx *gin.Context) { var request PostSignInRequestObject diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 5f3ebd6..93db937 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -10,6 +10,7 @@ paths: /sign-up: post: summary: Sign up a new user + operationId: postSignUp tags: [Auth] requestBody: required: true @@ -41,6 +42,7 @@ paths: /sign-in: post: summary: Sign in a user and return JWT + operationId: postSignIn tags: [Auth] requestBody: required: true @@ -73,88 +75,52 @@ paths: user_name: type: string "401": - description: Access denied due to invalid credentials + $ref: '#/components/responses/UnauthorizedError' + + /get-impersonation-token: + post: + summary: Get service impersontaion token + operationId: getImpersonationToken + tags: [Auth] + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user_id: + type: integer + format: int64 + tg_id: + type: integer + format: int64 + oneOf: + - required: ["user_id"] + - required: ["tg_id"] + responses: + "200": + description: Generated impersonation access token content: application/json: schema: type: object + required: + - access_token properties: - error: + access_token: type: string - example: "Access denied" - # /auth/verify-token: - # post: - # summary: Verify JWT validity - # tags: [Auth] - # requestBody: - # required: true - # content: - # application/json: - # schema: - # type: object - # required: [token] - # properties: - # token: - # type: string - # description: JWT token to validate - # responses: - # "200": - # description: Token validation result - # content: - # application/json: - # schema: - # type: object - # properties: - # valid: - # type: boolean - # description: True if token is valid - # user_id: - # type: string - # nullable: true - # description: User ID extracted from token if valid - # error: - # type: string - # nullable: true - # description: Error message if token is invalid - # /auth/refresh-token: - # post: - # summary: Refresh JWT using a refresh token - # tags: [Auth] - # requestBody: - # required: true - # content: - # application/json: - # schema: - # type: object - # required: [refresh_token] - # properties: - # refresh_token: - # type: string - # description: JWT refresh token obtained from sign-in - # responses: - # "200": - # description: New access (and optionally refresh) token - # content: - # application/json: - # schema: - # type: object - # properties: - # valid: - # type: boolean - # description: True if refresh token was valid - # user_id: - # type: string - # nullable: true - # description: User ID extracted from refresh token - # access_token: - # type: string - # description: New access token - # nullable: true - # refresh_token: - # type: string - # description: New refresh token (optional) - # nullable: true - # error: - # type: string - # nullable: true - # description: Error message if refresh token is invalid + description: JWT access token + "401": + $ref: '#/components/responses/UnauthorizedError' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + responses: + UnauthorizedError: + description: Access token is missing or invalid \ No newline at end of file diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 09907bc..39067a6 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -47,10 +47,28 @@ func CheckPassword(password, hash string) (bool, error) { return argon2id.ComparePasswordAndHash(password, hash) } +func (s Server) generateImpersonationToken(userID string, impersonated_by string) (accessToken string, err error) { + accessClaims := jwt.MapClaims{ + "user_id": userID, + "exp": time.Now().Add(15 * time.Minute).Unix(), + "imp_id": impersonated_by, + } + + at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) + + accessToken, err = at.SignedString(s.JwtPrivateKey) + if err != nil { + return "", err + } + + return accessToken, nil +} + func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) { accessClaims := jwt.MapClaims{ "user_id": userID, "exp": time.Now().Add(15 * time.Minute).Unix(), + //TODO: add created_at } at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessToken, err = at.SignedString(s.JwtPrivateKey) @@ -119,10 +137,7 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject // TODO: return 500 } if !ok { - err_msg := "invalid credentials" - return auth.PostSignIn401JSONResponse{ - Error: &err_msg, - }, nil + return auth.PostSignIn401Response{}, nil } accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname) @@ -144,6 +159,21 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject return result, nil } +func (s Server) GetImpersonationToken(ctx context.Context, request auth.GetImpersonationTokenRequestObject) (auth.GetImpersonationTokenResponseObject, error) { + ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) + if !ok { + log.Print("failed to get gin context") + // TODO: change to 500 + return auth.GetImpersonationToken200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") + } + + token := ginCtx.Request.Header.Get("Authorization") + log.Printf("got auth token: %s", token) + //s.db.GetExternalServiceByToken() + + return auth.PostSignIn401Response{}, nil +} + // func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) { // valid := false // var userID *string diff --git a/modules/auth/queries.sql b/modules/auth/queries.sql index 828d2af..363f07a 100644 --- a/modules/auth/queries.sql +++ b/modules/auth/queries.sql @@ -9,3 +9,7 @@ INTO users (passhash, nickname) VALUES (sqlc.arg(passhash), sqlc.arg(nickname)) RETURNING id; +-- name: GetExternalServiceByToken :one +SELECT * +FROM external_services +WHERE auth_token = sqlc.arg('auth_token'); \ No newline at end of file diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index 3499fe2..cda8d71 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -33,8 +33,6 @@ CREATE TABLE users ( last_login timestamptz ); - - CREATE TABLE studios ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, studio_name text NOT NULL UNIQUE, @@ -106,7 +104,8 @@ CREATE TABLE signals ( CREATE TABLE external_services ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - name text UNIQUE NOT NULL + name text UNIQUE NOT NULL, + auth_token text ); CREATE TABLE external_ids ( diff --git a/sql/models.go b/sql/models.go index 842d58c..ee30f58 100644 --- a/sql/models.go +++ b/sql/models.go @@ -193,8 +193,9 @@ type ExternalID struct { } type ExternalService struct { - ID int64 `json:"id"` - Name string `json:"name"` + ID int64 `json:"id"` + Name string `json:"name"` + AuthToken *string `json:"auth_token"` } type Image struct { diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 1cca986..7fd8765 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -74,6 +74,19 @@ func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams return i, err } +const getExternalServiceByToken = `-- name: GetExternalServiceByToken :one +SELECT id, name, auth_token +FROM external_services +WHERE auth_token = $1 +` + +func (q *Queries) GetExternalServiceByToken(ctx context.Context, authToken *string) (ExternalService, error) { + row := q.db.QueryRow(ctx, getExternalServiceByToken, authToken) + var i ExternalService + err := row.Scan(&i.ID, &i.Name, &i.AuthToken) + return i, err +} + const getImageByID = `-- name: GetImageByID :one SELECT id, storage_type, image_path FROM images