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 { repo *repository.Queries } func NewOAuthHandler(repo *repository.Queries) *OAuthHandler { return &OAuthHandler{ repo, } } func (h *OAuthHandler) RegisterRoutes(r chi.Router) { r.Post("/oauth/token", h.tokenEndpoint) r.Post("/oauth/code", h.getAuthCode) } 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 } 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 type Response struct { Code string `json:"code"` } encoder := json.NewEncoder(w) w.Header().Set("Content-Type", "application/json") if err := encoder.Encode(Response{ 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) // } // }