Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth Integration #891

Merged
merged 14 commits into from
May 25, 2024
Merged
24 changes: 0 additions & 24 deletions backend/auth/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,6 @@ type CustomClaims struct {
Role string `json:"role"`
}

type localsKey byte

const claimsKey localsKey = 0

// From extracts the CustomClaims from the fiber context
// Returns nil if the claims are not present
func From(c *fiber.Ctx) (*CustomClaims, error) {
rawClaims := c.Locals(claimsKey)
if rawClaims == nil {
return nil, utilities.Forbidden()
}

claims, ok := rawClaims.(*CustomClaims)
if !ok {
return nil, fmt.Errorf("claims are not of type CustomClaims. got: %T", rawClaims)
}

return claims, nil
}

func SetClaims(c *fiber.Ctx, claims *CustomClaims) {
c.Locals(claimsKey, claims)
}

type JWTType string

const (
Expand Down
52 changes: 52 additions & 0 deletions backend/auth/locals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package auth

import (
"fmt"

"github.com/GenerateNU/sac/backend/utilities"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)

type localsKey byte

const (
claimsKey localsKey = 0
userIDKey localsKey = 1
)

func CustomClaimsFrom(c *fiber.Ctx) (*CustomClaims, error) {
rawClaims := c.Locals(claimsKey)
if rawClaims == nil {
return nil, utilities.Forbidden()
}

claims, ok := rawClaims.(*CustomClaims)
if !ok {
return nil, fmt.Errorf("claims are not of type CustomClaims. got: %T", rawClaims)
}

return claims, nil
}

func SetClaims(c *fiber.Ctx, claims *CustomClaims) {
c.Locals(claimsKey, claims)
}

func UserIDFrom(c *fiber.Ctx) (*uuid.UUID, error) {
userID := c.Locals(userIDKey)
if userID == nil {
return nil, utilities.Forbidden()
}

id, ok := userID.(*uuid.UUID)
if !ok {
return nil, fmt.Errorf("userID is not of type uuid.UUID. got: %T", userID)
}

return id, nil
}

func SetUserID(c *fiber.Ctx, id *uuid.UUID) {
c.Locals(userIDKey, id)
}
20 changes: 11 additions & 9 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import (
)

type Settings struct {
Application ApplicationSettings
Database DatabaseSettings
SuperUser SuperUserSettings
Auth AuthSettings
AWS AWSSettings
Pinecone PineconeSettings
OpenAI OpenAISettings
Resend ResendSettings
Calendar CalendarSettings
Application ApplicationSettings
Database DatabaseSettings
SuperUser SuperUserSettings
Auth AuthSettings
AWS AWSSettings
Pinecone PineconeSettings
OpenAI OpenAISettings
Resend ResendSettings
Calendar CalendarSettings
GoogleSettings OAuthSettings
OutlookSettings OAuthSettings
}

type intermediateSettings struct {
Expand Down
14 changes: 14 additions & 0 deletions backend/config/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,19 @@ func readLocal(v *viper.Viper, path string, useDevDotEnv bool) (*Settings, error

settings.Resend = *resendSettings

googleSettings, err := readGoogleOAuthSettings()
if err != nil {
return nil, fmt.Errorf("failed to read Google OAuth settings: %w", err)
}

settings.GoogleSettings = *googleSettings

outlookSettings, err := readOutlookOAuthSettings()
if err != nil {
return nil, fmt.Errorf("failed to read Outlook OAuth settings: %w", err)
}

settings.OutlookSettings = *outlookSettings

return settings, nil
}
102 changes: 102 additions & 0 deletions backend/config/oauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package config

import (
"errors"
"os"

m "github.com/garrettladley/mattress"
)

type OAuthSettings struct {
BaseURL string
TokenURL string
ClientID *m.Secret[string]
ClientSecret *m.Secret[string]
Scopes string
RedirectURI string
ResponseType string
ResponseMode string
AccessType string
IncludeGrantedScopes string
Prompt string
}

type OAuthResources struct {
GoogleOAuthSettings *OAuthSettings
OutlookOAuthSettings *OAuthSettings
}

/**
* GOOGLE
**/
func readGoogleOAuthSettings() (*OAuthSettings, error) {
clientID := os.Getenv("GOOGLE_OAUTH_CLIENT_ID")
if clientID == "" {
return nil, errors.New("GOOGLE_OAUTH_CLIENT_ID is not set")
}

secretClientID, err := m.NewSecret(clientID)
if err != nil {
return nil, errors.New("failed to create secret from client ID")
}

clientSecret := os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET")
if clientSecret == "" {
return nil, errors.New("GOOGLE_OAUTH_CLIENT_SECRET is not set")
}

secretClientSecret, err := m.NewSecret(clientSecret)
if err != nil {
return nil, errors.New("failed to create secret from client secret")
}

return &OAuthSettings{
BaseURL: "https://accounts.google.com/o/oauth2/v2",
TokenURL: "https://oauth2.googleapis.com",
ClientID: secretClientID,
ClientSecret: secretClientSecret,
Scopes: "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly",
ResponseType: "code",
RedirectURI: "http://localhost:3000",
IncludeGrantedScopes: "true",
AccessType: "offline",
Prompt: "consent",
}, nil
}

/**
* OUTLOOK
**/
func readOutlookOAuthSettings() (*OAuthSettings, error) {
clientID := os.Getenv("OUTLOOK_OAUTH_CLIENT_ID")
if clientID == "" {
return nil, errors.New("OUTLOOK_OAUTH_CLIENT_ID is not set")
}

secretClientID, err := m.NewSecret(clientID)
if err != nil {
return nil, errors.New("failed to create secret from client ID")
}

clientSecret := os.Getenv("OUTLOOK_OAUTH_CLIENT_SECRET")
if clientSecret == "" {
return nil, errors.New("OUTLOOK_OAUTH_CLIENT_SECRET is not set")
}

secretClientSecret, err := m.NewSecret(clientSecret)
if err != nil {
return nil, errors.New("failed to create secret from client secret")
}

return &OAuthSettings{
BaseURL: "https://login.microsoftonline.com/common/oauth2/v2.0",
TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0",
ClientID: secretClientID,
ClientSecret: secretClientSecret,
Scopes: "offline_access user.read calendars.readwrite",
ResponseType: "code",
RedirectURI: "http://localhost:3000",
ResponseMode: "query",
Prompt: "consent",
}, nil
}
22 changes: 17 additions & 5 deletions backend/config/production.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ func readProd(v *viper.Viper) (*Settings, error) {
return nil, fmt.Errorf("failed to read Resend settings: %w", err)
}

googleSettings, err := readGoogleOAuthSettings()
if err != nil {
return nil, fmt.Errorf("failed to read Google OAuth settings: %w", err)
}

outlookSettings, err := readOutlookOAuthSettings()
if err != nil {
return nil, fmt.Errorf("failed to read Outlook OAuth settings: %w", err)
}

return &Settings{
Application: ApplicationSettings{
Port: uint16(portInt),
Expand All @@ -119,10 +129,12 @@ func readProd(v *viper.Viper) (*Settings, error) {
AccessKey: authAccessKey,
RefreshKey: authRefreshKey,
},
Pinecone: *pineconeSettings,
OpenAI: *openAISettings,
AWS: *awsSettings,
Resend: *resendSettings,
Calendar: prodSettings.Calendar,
Pinecone: *pineconeSettings,
OpenAI: *openAISettings,
AWS: *awsSettings,
Resend: *resendSettings,
Calendar: prodSettings.Calendar,
GoogleSettings: *googleSettings,
OutlookSettings: *outlookSettings,
}, nil
}
1 change: 1 addition & 0 deletions backend/constants/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "time"
const (
ACCESS_TOKEN_EXPIRY time.Duration = time.Minute * 24 * 30 // temporary TODO: change to 60 minutes
REFRESH_TOKEN_EXPIRY time.Duration = time.Minute * 24 * 30
CSRF_TOKEN_LENGTH int = 32
)

var SPECIAL_CHARACTERS = []rune{' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'} // see https://owasp.org/www-community/password-special-characters
35 changes: 35 additions & 0 deletions backend/entities/models/oauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package models

import (
"time"

"github.com/google/uuid"
)

type OAuthResource string

const (
Google OAuthResource = "google"
Outlook OAuthResource = "outlook"
)

type UserOAuthTokens struct {
UserID uuid.UUID `json:"user_id" validate:"required,uuid4"`
RefreshToken string `json:"refresh_token" validate:"max=255"`
AccessToken string `json:"access_token" validate:"max=255"`
CSRFToken string `json:"csrf_token" validate:"max=255"`
ResourceType OAuthResource `json:"resource_type" validate:"required"`
ExpiresAt time.Time `json:"expires_at" validate:"required"`
}

type OAuthToken struct {
AccessToken string `json:"access_token" validate:"required"`
ExpiresIn int `json:"expires_in" validate:"required"`
RefreshToken string `json:"refresh_token" validate:"required"`
Scope string `json:"scope" validate:"required"`
TokenType string `json:"token_type" validate:"required"`
}

func (UserOAuthTokens) TableName() string {
return "user_oauth_tokens"
}
Loading
Loading