From 570ae6ac8c0c88ee2f4c4b38a332782d90654883 Mon Sep 17 00:00:00 2001 From: LandaMm Date: Sat, 7 Jun 2025 19:17:15 +0200 Subject: [PATCH] feat: generate 3 tokens for api service --- internal/oauth/token.go | 233 +++++++++++++++++++++++++++++++++------- 1 file changed, 193 insertions(+), 40 deletions(-) diff --git a/internal/oauth/token.go b/internal/oauth/token.go index 0384efa..215126b 100644 --- a/internal/oauth/token.go +++ b/internal/oauth/token.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/types" "gitea.local/admin/hspguard/internal/util" "gitea.local/admin/hspguard/internal/web" @@ -16,6 +17,102 @@ import ( "github.com/google/uuid" ) +type ApiToken struct { + Token string + Expiration float64 +} + +type ApiTokens struct { + ID ApiToken + Access ApiToken + Refresh ApiToken +} + +func (h *OAuthHandler) signApiTokens(user *repository.User, apiService *repository.ApiService, nonce *string) (*ApiTokens, error) { + accessExpiresIn := 15 * time.Minute + accessExpiresAt := time.Now().Add(accessExpiresIn) + + accessClaims := types.ApiClaims{ + Permissions: []string{}, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: h.cfg.Uri, + Subject: apiService.ClientID, + Audience: jwt.ClaimStrings{apiService.ClientID}, + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(accessExpiresAt), + }, + } + + access, err := util.SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey) + if err != nil { + return nil, err + } + + var roles = []string{"user"} + + if user.IsAdmin { + roles = append(roles, "admin") + } + + idExpiresIn := 15 * time.Minute + idExpiresAt := time.Now().Add(idExpiresIn) + + idClaims := types.IdTokenClaims{ + Email: user.Email, + EmailVerified: user.EmailVerified, + Name: user.FullName, + Picture: user.ProfilePicture, + Nonce: nonce, + Roles: roles, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: h.cfg.Uri, + Subject: user.ID.String(), + Audience: jwt.ClaimStrings{apiService.ClientID}, + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(idExpiresAt), + }, + } + + idToken, err := util.SignJwtToken(idClaims, h.cfg.Jwt.PrivateKey) + if err != nil { + return nil, err + } + + refreshExpiresIn := 24 * time.Hour + refreshExpiresAt := time.Now().Add(refreshExpiresIn) + + refreshClaims := types.ApiRefreshClaims{ + UserID: user.ID.String(), + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: h.cfg.Uri, + Subject: apiService.ClientID, + Audience: jwt.ClaimStrings{apiService.ClientID}, + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(refreshExpiresAt), + }, + } + + refresh, err := util.SignJwtToken(refreshClaims, h.cfg.Jwt.PrivateKey) + if err != nil { + return nil, err + } + + return &ApiTokens{ + ID: ApiToken{ + Token: idToken, + Expiration: idExpiresIn.Seconds(), + }, + Access: ApiToken{ + Token: access, + Expiration: accessExpiresIn.Seconds(), + }, + Refresh: ApiToken{ + Token: refresh, + Expiration: refreshExpiresIn.Seconds(), + }, + }, nil +} + func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) { log.Println("[OAUTH] New request to token endpoint") @@ -65,65 +162,121 @@ func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) { fmt.Printf("Code received: %s\n", code) - // TODO: Verify code from another db table - nonce := strings.Split(code, ",")[1] + session, err := h.cache.GetAuthCode(r.Context(), code) + if err != nil { + log.Printf("ERR: Failed to find session under the code %s: %v\n", code, err) + web.Error(w, "no session found under this auth code", http.StatusNotFound) + return + } - userId := strings.Split(code, ",")[0] + log.Printf("DEBUG: Fetched code session: %#v\n", session) - user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) + apiService, err := h.repo.GetApiServiceCID(r.Context(), session.ClientID) + if err != nil { + log.Printf("ERR: Could not find API service with client %s: %v\n", session.ClientID, err) + web.Error(w, "service is not registered", http.StatusForbidden) + return + } + + if session.ClientID != clientId { + web.Error(w, "invalid auth", http.StatusUnauthorized) + return + } + + user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(session.UserID)) if err != nil { web.Error(w, "requested user not found", http.StatusNotFound) return } - var roles = []string{"user"} - - if user.IsAdmin { - roles = append(roles, "admin") - } - - claims := types.ApiClaims{ - Email: user.Email, - // TODO: - EmailVerified: true, - Name: user.FullName, - Picture: user.ProfilePicture, - Nonce: nonce, - Roles: roles, - RegisteredClaims: jwt.RegisteredClaims{ - Issuer: h.cfg.Uri, - // TODO: use dedicated API id that is in local DB and bind to user there - Subject: user.ID.String(), - Audience: jwt.ClaimStrings{clientId}, - IssuedAt: jwt.NewNumericDate(time.Now()), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), - }, - } - - idToken, err := util.SignJwtToken(claims, h.cfg.Jwt.PrivateKey) + tokens, err := h.signApiTokens(&user, &apiService, &session.Nonce) if err != nil { - web.Error(w, "failed to sign id token", http.StatusInternalServerError) + log.Println("ERR: Failed to sign api tokens:", err) + web.Error(w, "failed to sign tokens", http.StatusInternalServerError) return } type Response struct { - IdToken string `json:"id_token"` - TokenType string `json:"token_type"` - AccessToken string `json:"access_token"` - Email string `json:"email"` - // TODO: add expires_in, refresh_token, scope (RFC 8693 $2) + IdToken string `json:"id_token"` + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + Email string `json:"email"` + RefreshToken string `json:"refresh_token"` + ExpiresIn float64 `json:"expires_in"` + // TODO: add scope (RFC 8693 $2) } response := Response{ - IdToken: idToken, - TokenType: "Bearer", - // FIXME: - AccessToken: idToken, - Email: user.Email, + IdToken: tokens.ID.Token, + TokenType: "Bearer", + AccessToken: tokens.Access.Token, + RefreshToken: tokens.Refresh.Token, + ExpiresIn: tokens.Access.Expiration, + Email: user.Email, } log.Printf("sending following response: %#v\n", response) + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + if err := encoder.Encode(response); err != nil { + web.Error(w, "failed to encode response", http.StatusInternalServerError) + } + case "refresh_token": + refreshToken := r.FormValue("refresh_token") + + var claims types.ApiRefreshClaims + + token, err := util.VerifyToken(refreshToken, h.cfg.Jwt.PublicKey, &claims) + if err != nil || !token.Valid { + http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized) + return + } + + expire, err := claims.GetExpirationTime() + if err != nil { + web.Error(w, "failed to retrieve enough info from the token", http.StatusInternalServerError) + return + } + + if time.Now().After(expire.Time) { + web.Error(w, "token is expired", http.StatusUnauthorized) + return + } + + userID, err := uuid.Parse(claims.UserID) + if err != nil { + web.Error(w, "invalid user credentials in refresh token", http.StatusBadRequest) + return + } + user, err := h.repo.FindUserId(r.Context(), userID) + + apiService, err := h.repo.GetApiServiceCID(r.Context(), claims.Subject) + if err != nil { + web.Error(w, "api service is not registered", http.StatusUnauthorized) + return + } + + tokens, err := h.signApiTokens(&user, &apiService, nil) + + type Response struct { + IdToken string `json:"id_token"` + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn float64 `json:"expires_in"` + } + + response := Response{ + IdToken: tokens.ID.Token, + TokenType: "Bearer", + AccessToken: tokens.Access.Token, + RefreshToken: tokens.Refresh.Token, + ExpiresIn: tokens.Access.Expiration, + } + + log.Printf("DEBUG: refresh - sending following response: %#v\n", response) + w.Header().Set("Content-Type", "application/json") encoder := json.NewEncoder(w) if err := encoder.Encode(response); err != nil {