feat: generate 3 tokens for api service

This commit is contained in:
2025-06-07 19:17:15 +02:00
parent f0d3a61e7b
commit 570ae6ac8c

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/types" "gitea.local/admin/hspguard/internal/types"
"gitea.local/admin/hspguard/internal/util" "gitea.local/admin/hspguard/internal/util"
"gitea.local/admin/hspguard/internal/web" "gitea.local/admin/hspguard/internal/web"
@ -16,6 +17,102 @@ import (
"github.com/google/uuid" "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) { func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
log.Println("[OAUTH] New request to token endpoint") log.Println("[OAUTH] New request to token endpoint")
@ -65,44 +162,37 @@ func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Code received: %s\n", code) fmt.Printf("Code received: %s\n", code)
// TODO: Verify code from another db table session, err := h.cache.GetAuthCode(r.Context(), code)
nonce := strings.Split(code, ",")[1] 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 { if err != nil {
web.Error(w, "requested user not found", http.StatusNotFound) web.Error(w, "requested user not found", http.StatusNotFound)
return return
} }
var roles = []string{"user"} tokens, err := h.signApiTokens(&user, &apiService, &session.Nonce)
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)
if err != nil { 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 return
} }
@ -111,19 +201,82 @@ func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
TokenType string `json:"token_type"` TokenType string `json:"token_type"`
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
Email string `json:"email"` Email string `json:"email"`
// TODO: add expires_in, refresh_token, scope (RFC 8693 $2) RefreshToken string `json:"refresh_token"`
ExpiresIn float64 `json:"expires_in"`
// TODO: add scope (RFC 8693 $2)
} }
response := Response{ response := Response{
IdToken: idToken, IdToken: tokens.ID.Token,
TokenType: "Bearer", TokenType: "Bearer",
// FIXME: AccessToken: tokens.Access.Token,
AccessToken: idToken, RefreshToken: tokens.Refresh.Token,
ExpiresIn: tokens.Access.Expiration,
Email: user.Email, Email: user.Email,
} }
log.Printf("sending following response: %#v\n", response) 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") w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
if err := encoder.Encode(response); err != nil { if err := encoder.Encode(response); err != nil {