woj-server/internal/api/oauth/callback.go

115 lines
2.9 KiB
Go

package oauth
import (
"context"
"fmt"
userApi "git.0x7f.app/WOJ/woj-server/internal/api/user"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin"
)
// CallbackHandler
// @Summary Callback with OAuth2
// @Description Callback endpoint from OAuth2
// @Tags oauth
// @Produce json
// @Router /oauth/callback [get]
func (h *handler) CallbackHandler() gin.HandlerFunc {
// TODO: we are returning e.Response directly here, we should redirect to a trampoline page, passing the response as query string
return func(c *gin.Context) {
// Extract key from cookie
key, err := c.Cookie(oauthStateCookieName)
if err != nil {
e.Pong[any](c, e.InvalidParameter, nil)
return
}
// Get state from redis
key = fmt.Sprintf(oauthStateKey, key)
expected, err := h.cache.Get().Get(context.Background(), key).Result()
if err != nil {
e.Pong[any](c, e.RedisError, nil)
return
}
// Whether state is valid, delete it
h.cache.Get().Unlink(context.Background(), key)
c.SetCookie(oauthStateCookieName, "", -1, "/", "", false, true)
// Verify state
if c.Query("state") != expected {
e.Pong[any](c, e.OAuthStateMismatch, nil)
return
}
// Exchange code for token
token, err := h.conf.Exchange(context.Background(), c.Query("code"))
if err != nil {
e.Pong[any](c, e.OAuthExchangeFailed, nil)
return
}
// Extract the ID Token from OAuth2 token.
raw, ok := token.Extra("id_token").(string)
if !ok {
e.Pong[any](c, e.OAuthExchangeFailed, nil)
return
}
// Parse and verify ID Token payload.
idToken, err := h.verifier.Verify(context.Background(), raw)
if err != nil {
e.Pong[any](c, e.OAuthVerifyFailed, nil)
return
}
// Extract custom claims
// TODO: extract role from claims
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Nickname string `json:"preferred_username"`
Role string `json:"role"`
}
if err := idToken.Claims(&claims); err != nil {
e.Pong[any](c, e.OAuthGetClaimsFailed, nil)
return
}
if !claims.EmailVerified || claims.Email == "" || claims.Nickname == "" {
e.Pong[any](c, e.UserInvalid, nil)
return
}
// Check user existence
u, status := h.user.ProfileOrCreate(&user.CreateData{Email: claims.Email, NickName: claims.Nickname})
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// Increment user version
version, status := h.user.IncrVersion(u.ID)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// Sign JWT token
claim := &model.Claim{
UID: u.ID,
Role: u.Role,
Version: version,
}
jwt, status := h.jwt.SignClaim(claim)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
e.Pong(c, status, userApi.LoginResponse{Token: jwt, NickName: u.NickName})
}
}