feat: generate 3 tokens for api service
This commit is contained in:
@ -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,65 +162,121 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
IdToken string `json:"id_token"`
|
IdToken string `json:"id_token"`
|
||||||
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,
|
||||||
Email: user.Email,
|
ExpiresIn: tokens.Access.Expiration,
|
||||||
|
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 {
|
||||||
|
Reference in New Issue
Block a user