feat: generate 3 tokens for api service
This commit is contained in:
@ -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 {
|
||||
|
Reference in New Issue
Block a user