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
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
}
12 changes: 12 additions & 0 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 @@ -124,5 +134,7 @@ func readProd(v *viper.Viper) (*Settings, error) {
AWS: *awsSettings,
Resend: *resendSettings,
Calendar: prodSettings.Calendar,
GoogleSettings: *googleSettings,
OutlookSettings: *outlookSettings,
}, nil
}
40 changes: 40 additions & 0 deletions backend/entities/models/oauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package models

import (
"time"

"github.com/google/uuid"
)

type OAuthResource string

const (
Google OAuthResource = "google"

Check failure on line 12 in backend/entities/models/oauth.go

View workflow job for this annotation

GitHub Actions / Lint

File is not `goimports`-ed (goimports)
Outlook OAuthResource = "outlook"
)

type OAuthTokenRequestBody struct {
Code string `json:"code" validate:"omitempty"`
State string `json:"state" validate:"omitempty"`
}

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"
}
92 changes: 92 additions & 0 deletions backend/entities/oauth/base/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package base

import (
"errors"
"net/http"

"github.com/GenerateNU/sac/backend/entities/models"
"github.com/GenerateNU/sac/backend/utilities"
"github.com/gofiber/fiber/v2"
)

type OAuthController struct {
OAuthService OAuthServiceInterface
}

func NewOAuthController(oauthService OAuthServiceInterface) *OAuthController {
return &OAuthController{OAuthService: oauthService}
}

func (oc *OAuthController) Authorize(c *fiber.Ctx) error {
// Extract the resource type from the query params:
resourceType := models.OAuthResource(c.Query("type"))
if resourceType == "" {
return errors.New("resource type is required")
}

// Extract the user making the call:
userID := c.Locals("userID").(string)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use a private type as the key to avoid any potential collisions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, isn't this stored in the custom claims as the issuer? it is set here when logging in

idAsUUID, err := utilities.ValidateID(userID)
if err != nil {
return err
}

// Call the respective authorize method:
authUrl, err := oc.OAuthService.Authorize(*idAsUUID, resourceType)
if err != nil {
return err
}

return c.Status(fiber.StatusOK).JSON(*authUrl)
}

func (oc *OAuthController) Token(c *fiber.Ctx) (error) {
// Extract the resource type from the query params:
resourceType := models.OAuthResource(c.Query("type"))
if resourceType == "" {
return errors.New("resource type is required")
}

// Parse the body of the request:
var tokenBody models.OAuthTokenRequestBody
if err := c.BodyParser(&tokenBody); err != nil {
return err
}

// Extract the user making the call:
userID := c.Locals("userID").(string)
idAsUUID, err := utilities.ValidateID(userID)
if err != nil {
return err
}

// Call the respective token method:
err = oc.OAuthService.Token(*idAsUUID, tokenBody, resourceType)
if err != nil {
return err
}

return c.SendStatus(http.StatusNoContent)
}

func (oc *OAuthController) Revoke(c *fiber.Ctx) error {
// Extract the resource type from the query params:
resourceType := models.OAuthResource(c.Query("type"))
if resourceType == "" {
return errors.New("resource type is required")
}

// Extract the user making the call:
userID := c.Locals("userID").(string)
idAsUUID, err := utilities.ValidateID(userID)
if err != nil {
return err
}

err = oc.OAuthService.Revoke(*idAsUUID, resourceType)
if err != nil {
return err
}

return c.Status(fiber.StatusOK).JSON("Successfully revoked token")
}
14 changes: 14 additions & 0 deletions backend/entities/oauth/base/routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package base

import "github.com/GenerateNU/sac/backend/types"

func OAuth(params types.RouteParams) {
oauthController := NewOAuthController(&OAuthService{ ServiceParams: params.ServiceParams })

// api/v1/calendar/*
calendar := params.Router.Group("/oauth")

calendar.Get("/authorize", params.AuthMiddleware.UserAuthorizeAndExtractID, oauthController.Authorize)
calendar.Post("/token", params.AuthMiddleware.UserAuthorizeAndExtractID, oauthController.Token)
calendar.Delete("/revoke", params.AuthMiddleware.UserAuthorizeAndExtractID, oauthController.Revoke)
}
Loading
Loading