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"
"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 {