diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index cc64d33..3f9b560 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -76,6 +76,7 @@ func (a *api) Routes() *chi.Mux { a.registerAuthRoutes(r, api) a.registerCropRoutes(r, api) a.registerPlantRoutes(r, api) + a.registerOauthRoutes(r, api) }) router.Group(func(r chi.Router) { diff --git a/backend/internal/api/oauth.go b/backend/internal/api/oauth.go new file mode 100644 index 0000000..4b61ca7 --- /dev/null +++ b/backend/internal/api/oauth.go @@ -0,0 +1,58 @@ +package api + +import ( + "context" + "errors" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/forfarm/backend/internal/utilities" + "github.com/go-chi/chi/v5" +) + +func (a *api) registerOauthRoutes(_ chi.Router, apiInstance huma.API) { + tags := []string{"oauth"} + + huma.Register(apiInstance, huma.Operation{ + OperationID: "oauth_exchange", + Method: http.MethodPost, + Path: "/oauth/exchange", + Tags: tags, + }, a.exchangeHandler) +} + +type ExchangeTokenInput struct { + Body struct { + AccessToken string `json:"access_token" example:"Google ID token obtained after login"` + } +} + +type ExchangeTokenOutput struct { + Body struct { + JWT string `json:"jwt" example:"Fresh JWT for frontend authentication"` + Email string `json:"email" example:"Email address of the user"` + } +} + +// exchangeHandler now assumes the provided access token is a Google ID token. +// It verifies the token with Google and then generates your own JWT. +func (a *api) exchangeHandler(ctx context.Context, input *ExchangeTokenInput) (*ExchangeTokenOutput, error) { + if input.Body.AccessToken == "" { + return nil, errors.New("access token is required") + } + + googleUserID, email, err := utilities.ExtractGoogleUserID(input.Body.AccessToken) + if err != nil { + return nil, errors.New("invalid Google ID token") + } + + newJWT, err := utilities.CreateJwtToken(googleUserID) + if err != nil { + return nil, err + } + + resp := &ExchangeTokenOutput{} + resp.Body.JWT = newJWT + resp.Body.Email = email + return resp, nil +} diff --git a/backend/internal/utilities/jwt.go b/backend/internal/utilities/jwt.go index f5cdc8f..caeba1d 100644 --- a/backend/internal/utilities/jwt.go +++ b/backend/internal/utilities/jwt.go @@ -8,7 +8,8 @@ import ( "github.com/golang-jwt/jwt/v5" ) -var deafultSecretKey = []byte(config.JWT_SECRET_KEY) +// TODO: Change later +var defaultSecretKey = []byte(config.JWT_SECRET_KEY) func CreateJwtToken(uuid string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ @@ -16,7 +17,7 @@ func CreateJwtToken(uuid string) (string, error) { "exp": time.Now().Add(time.Hour * 24).Unix(), }) - tokenString, err := token.SignedString(deafultSecretKey) + tokenString, err := token.SignedString(defaultSecretKey) if err != nil { return "", err } @@ -25,7 +26,7 @@ func CreateJwtToken(uuid string) (string, error) { } func VerifyJwtToken(tokenString string, customKey ...[]byte) error { - secretKey := deafultSecretKey + secretKey := defaultSecretKey if len(customKey) > 0 { if len(customKey[0]) < 32 { return errors.New("provided key is too short, minimum length is 32 bytes") @@ -52,25 +53,40 @@ func VerifyJwtToken(tokenString string, customKey ...[]byte) error { return nil } -// ExtractUUIDFromToken decodes the JWT token using the default secret key, -// and returns the uuid claim contained within the token. -func ExtractUUIDFromToken(tokenString string) (string, error) { +func ExtractUUIDFromToken(tokenString string, customKey ...[]byte) (string, error) { + secretKey := defaultSecretKey + if len(customKey) > 0 { + if len(customKey[0]) < 32 { + return "", errors.New("provided key is too short, minimum length is 32 bytes") + } + secretKey = customKey[0] + } + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } - return deafultSecretKey, nil + + return secretKey, nil }) + if err != nil { return "", err } - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - if uuid, ok := claims["uuid"].(string); ok { - return uuid, nil - } - return "", errors.New("uuid not found in token") + if !token.Valid { + return "", jwt.ErrSignatureInvalid } - return "", errors.New("invalid token claims") + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return "", errors.New("unable to parse claims") + } + + userID, ok := claims["uuid"].(string) + if !ok || userID == "" { + return "", errors.New("uuid claim is missing or invalid") + } + + return userID, nil } diff --git a/backend/internal/utilities/oauth.go b/backend/internal/utilities/oauth.go new file mode 100644 index 0000000..6fa7b5e --- /dev/null +++ b/backend/internal/utilities/oauth.go @@ -0,0 +1,41 @@ +package utilities + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type GoogleTokenInfo struct { + Sub string `json:"sub"` // Unique Google user identifier. + Email string `json:"email"` // The user's email address. +} + +func ExtractGoogleUserID(idToken string) (string, string, error) { + if idToken == "" { + return "", "", errors.New("provided id token is empty") + } + + url := fmt.Sprintf("https://oauth2.googleapis.com/tokeninfo?id_token=%s", idToken) + resp, err := http.Get(url) + if err != nil { + return "", "", fmt.Errorf("error verifying Google ID token: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", "", fmt.Errorf("tokeninfo endpoint returned unexpected status: %d", resp.StatusCode) + } + + var tokenInfo GoogleTokenInfo + if err := json.NewDecoder(resp.Body).Decode(&tokenInfo); err != nil { + return "", "", fmt.Errorf("error decoding token info response: %w", err) + } + + if tokenInfo.Sub == "" { + return "", "", errors.New("Google token missing 'sub' claim") + } + + return tokenInfo.Sub, tokenInfo.Email, nil +} diff --git a/frontend/app/auth/signin/google-oauth.tsx b/frontend/app/auth/signin/google-oauth.tsx index 8d5f1da..92d10e4 100644 --- a/frontend/app/auth/signin/google-oauth.tsx +++ b/frontend/app/auth/signin/google-oauth.tsx @@ -1,10 +1,48 @@ -import Image from "next/image"; +import React, { useContext } from "react"; +import { useRouter } from "next/navigation"; +import { GoogleLogin, CredentialResponse } from "@react-oauth/google"; +import { SessionContext } from "@/context/SessionContext"; + +interface OAuthExchangeData { + jwt: string; + email: string; +} export function GoogleSigninButton() { - return ( -