package oauth import ( "context" "crypto/rand" "encoding/base64" "time" "github.com/porter-dev/porter/internal/models/integrations" "github.com/porter-dev/porter/internal/repository" "golang.org/x/oauth2" ) type Config struct { ClientID string ClientSecret string Scopes []string BaseURL string } // GithubAppConf is standard oauth2 config but it need to keeps track of the app name and webhook secret type GithubAppConf struct { AppName string WebhookSecret string SecretPath string AppID int64 oauth2.Config } const ( GithubAuthURL string = "https://github.com/login/oauth/authorize" GithubTokenURL string = "https://github.com/login/oauth/access_token" DOAuthURL string = "https://cloud.digitalocean.com/v1/oauth/authorize" DOTokenURL string = "https://cloud.digitalocean.com/v1/oauth/token" GoogleAuthURL string = "https://accounts.google.com/o/oauth2/v2/auth" GoogleTokenURL string = "https://oauth2.googleapis.com/token" SlackAuthURL string = "https://slack.com/oauth/v2/authorize" SlackTokenURL string = "https://slack.com/api/oauth.v2.access" ) func NewGithubClient(cfg *Config) *oauth2.Config { return &oauth2.Config{ ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: GithubAuthURL, TokenURL: GithubTokenURL, }, RedirectURL: cfg.BaseURL + "/api/oauth/github/callback", Scopes: cfg.Scopes, } } func NewGithubAppClient(cfg *Config, name string, secret string, secretPath string, appID int64) *GithubAppConf { return &GithubAppConf{ AppName: name, WebhookSecret: secret, SecretPath: secretPath, AppID: appID, Config: oauth2.Config{ ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: GithubAuthURL, TokenURL: GithubTokenURL, }, RedirectURL: cfg.BaseURL + "/api/oauth/github-app/callback", Scopes: cfg.Scopes, }, } } func NewDigitalOceanClient(cfg *Config) *oauth2.Config { return &oauth2.Config{ ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: DOAuthURL, TokenURL: DOTokenURL, AuthStyle: oauth2.AuthStyleInParams, }, RedirectURL: cfg.BaseURL + "/api/oauth/digitalocean/callback", Scopes: cfg.Scopes, } } func NewGoogleClient(cfg *Config) *oauth2.Config { return &oauth2.Config{ ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: GoogleAuthURL, TokenURL: GoogleTokenURL, }, RedirectURL: cfg.BaseURL + "/api/oauth/google/callback", Scopes: cfg.Scopes, } } func NewSlackClient(cfg *Config) *oauth2.Config { return &oauth2.Config{ ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: SlackAuthURL, TokenURL: SlackTokenURL, }, RedirectURL: cfg.BaseURL + "/api/oauth/slack/callback", Scopes: cfg.Scopes, } } func CreateRandomState() string { b := make([]byte, 16) rand.Read(b) state := base64.URLEncoding.EncodeToString(b) return state } // MakeUpdateOAuthIntegrationTokenFunction creates a function to be passed to GetAccessToken that updates the OauthIntegration // if it needs to be updated func MakeUpdateOAuthIntegrationTokenFunction( o *integrations.OAuthIntegration, repo repository.Repository) func(accessToken []byte, refreshToken []byte, expiry time.Time) error { return func(accessToken []byte, refreshToken []byte, expiry time.Time) error { o.AccessToken = accessToken o.RefreshToken = refreshToken o.Expiry = expiry _, err := repo.OAuthIntegration().UpdateOAuthIntegration(o) return err } } // MakeUpdateGithubAppOauthIntegrationFunction creates a function to be passed to GetAccessToken that updates the GithubAppOauthIntegration // if it needs to be updated func MakeUpdateGithubAppOauthIntegrationFunction( o *integrations.GithubAppOAuthIntegration, repo repository.Repository) func(accessToken []byte, refreshToken []byte, expiry time.Time) error { return func(accessToken []byte, refreshToken []byte, expiry time.Time) error { o.AccessToken = accessToken o.RefreshToken = refreshToken o.Expiry = expiry _, err := repo.GithubAppOAuthIntegration().UpdateGithubAppOauthIntegration(o) return err } } // MakeUpdateGitlabAppOAuthIntegrationFunction creates a function to be passed to GetAccessToken that updates the GitlabAppOAuthIntegration // if it needs to be updated func MakeUpdateGitlabAppOAuthIntegrationFunction( projectID uint, o *integrations.GitlabAppOAuthIntegration, repo repository.Repository, ) func(accessToken []byte, refreshToken []byte, expiry time.Time) error { return func(accessToken []byte, refreshToken []byte, expiry time.Time) error { o, err := repo.OAuthIntegration().ReadOAuthIntegration(projectID, o.OAuthIntegrationID) if err != nil { return err } o.AccessToken = accessToken o.RefreshToken = refreshToken o.Expiry = expiry _, err = repo.OAuthIntegration().UpdateOAuthIntegration(o) return err } } // GetAccessToken retrieves an access token for a given client. It updates the // access token in the DB if necessary func GetAccessToken( prevToken integrations.SharedOAuthModel, conf *oauth2.Config, updateToken func(accessToken []byte, refreshToken []byte, expiry time.Time) error, ) (string, *time.Time, error) { expiry := prevToken.Expiry if conf.Endpoint.AuthURL == DOAuthURL && expiry.IsZero() { // manually set the expiry so refresh token is used expiry = time.Now().Add(-1 * time.Minute) } tokSource := conf.TokenSource(context.TODO(), &oauth2.Token{ AccessToken: string(prevToken.AccessToken), RefreshToken: string(prevToken.RefreshToken), TokenType: "Bearer", Expiry: expiry, }) token, err := tokSource.Token() if err != nil { return "", nil, err } if token.AccessToken != string(prevToken.AccessToken) { err := updateToken([]byte(token.AccessToken), []byte(token.RefreshToken), token.Expiry) if err != nil { return "", nil, err } } return token.AccessToken, &token.Expiry, nil }