From 6e2d67ad240afca1fb020b43e699426d74581acb Mon Sep 17 00:00:00 2001 From: LandaMm Date: Sun, 25 May 2025 14:16:20 +0200 Subject: [PATCH] feat: oauth endpoints: `code` and `token` --- internal/oauth/routes.go | 201 +++++++++++++++++++++++++++++++++++---- 1 file changed, 181 insertions(+), 20 deletions(-) diff --git a/internal/oauth/routes.go b/internal/oauth/routes.go index 0633abc..8a80f32 100644 --- a/internal/oauth/routes.go +++ b/internal/oauth/routes.go @@ -1,49 +1,210 @@ package oauth import ( + "encoding/base64" "encoding/json" + "fmt" "log" "net/http" + "strings" + "time" + "gitea.local/admin/hspguard/internal/auth" + "gitea.local/admin/hspguard/internal/repository" + "gitea.local/admin/hspguard/internal/types" + "gitea.local/admin/hspguard/internal/util" "gitea.local/admin/hspguard/internal/web" "github.com/go-chi/chi/v5" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) -type OAuthHandler struct{} +type OAuthHandler struct { + repo *repository.Queries +} -func NewOAuthHandler() *OAuthHandler { - return &OAuthHandler{} +func NewOAuthHandler(repo *repository.Queries) *OAuthHandler { + return &OAuthHandler{ + repo, + } } func (h *OAuthHandler) RegisterRoutes(r chi.Router) { - r.Get("/oauth/authorize", h.authorizeEndpoint) - r.Get("/oauth/token", h.tokenEndpoint) + r.Post("/oauth/token", h.tokenEndpoint) + + r.Post("/oauth/code", h.getAuthCode) } -func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) { - log.Println("[OAUTH] New request to token endpoint") - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) -} +func (h *OAuthHandler) getAuthCode(w http.ResponseWriter, r *http.Request) { + userId, ok := util.GetRequestUserId(r.Context()) + if !ok { + web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError) + return + } -func (h *OAuthHandler) authorizeEndpoint(w http.ResponseWriter, r *http.Request) { - log.Println("[OAUTH] New request to authorize endpoint") - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) -} + user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) + if err != nil { + web.Error(w, "user with provided id does not exist", http.StatusUnauthorized) + return + } + + type Request struct { + Nonce string `json:"nonce"` + } + + var req Request + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&req); err != nil { + web.Error(w, "nonce field is required in request", http.StatusBadRequest) + return + } + + // TODO: Create real authorization code -func (h *OAuthHandler) Metadata(w http.ResponseWriter, r *http.Request) { type Response struct { - TokenEndpoint string `json:"token_endpoint"` - AuthEndpoint string `json:"authorization_endpoint"` + Code string `json:"code"` } encoder := json.NewEncoder(w) + w.Header().Set("Content-Type", "application/json") + if err := encoder.Encode(Response{ - TokenEndpoint: "http://192.168.178.21:3001/api/v1/oauth/token", - AuthEndpoint: "http://192.168.178.21:5173/authorize", + Code: fmt.Sprintf("%s,%s", user.ID.String(), req.Nonce), }); err != nil { web.Error(w, "failed to encode response", http.StatusInternalServerError) } } + +func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) { + log.Println("[OAUTH] New request to token endpoint") + + authHeader := r.Header.Get("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Decode credentials + payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic ")) + if err != nil { + http.Error(w, "Invalid auth encoding", http.StatusBadRequest) + return + } + + var clientId string + var clientSecret string + + parts := strings.SplitN(string(payload), ":", 2) + if len(parts) != 2 { + http.Error(w, "Unauthorized", http.StatusForbidden) + return + } + + clientId = parts[0] + clientSecret = parts[1] + + log.Printf("Some client is trying to exchange code with id: %s and secret: %s\n", clientId, clientSecret) + + // Parse the form data + err = r.ParseForm() + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + grantType := r.FormValue("grant_type") + redirectUri := r.FormValue("redirect_uri") + + log.Printf("Redirect URI is %s\n", redirectUri) + + switch grantType { + case "authorization_code": + code := r.FormValue("code") + + fmt.Printf("Code received: %s\n", code) + + // TODO: Verify code from another db table + nonce := strings.Split(code, ",")[1] + + userId := strings.Split(code, ",")[0] + + user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) + if err != nil { + web.Error(w, "requested user not found", http.StatusNotFound) + return + } + + claims := types.ApiClaims{ + Email: user.Email, + // TODO: + EmailVerified: true, + Name: user.FullName, + Picture: user.ProfilePicture.String, + Nonce: nonce, + Roles: []string{"user"}, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app", + // 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 := auth.SignJwtToken(claims) + if err != nil { + web.Error(w, "failed to sign id token", 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) + } + + response := Response{ + IdToken: idToken, + TokenType: "Bearer", + // FIXME: + AccessToken: idToken, + 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) + } + default: + web.Error(w, "unsupported grant type", http.StatusBadRequest) + } +} + +// func (h *OAuthHandler) Metadata(w http.ResponseWriter, r *http.Request) { +// type Response struct { +// TokenEndpoint string `json:"token_endpoint"` +// AuthEndpoint string `json:"authorization_endpoint"` +// Issuer string `json:"issuer"` +// JwksUri string `json:"jwks_uri"` +// } + +// encoder := json.NewEncoder(w) + +// if err := encoder.Encode(Response{ +// TokenEndpoint: "http://192.168.178.21:3001/api/v1/oauth/token", +// AuthEndpoint: "https://147f-2a00-10-5b00-c801-4882-7ef0-5e68-d360.ngrok-free.app/authorize", +// JwksUri: "http://192.168.178.21:3001/.well-known/jwks.json", +// Issuer: "http://192.168.178.21:3001", +// }); err != nil { +// web.Error(w, "failed to encode response", http.StatusInternalServerError) +// } +// }