-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add github sso to dashboard (#373)
- Loading branch information
1 parent
a5a2629
commit f16a9cd
Showing
22 changed files
with
1,326 additions
and
193 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package users | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
|
||
githubsdk "github.com/google/go-github/v57/github" | ||
"github.com/labstack/echo/v4" | ||
"golang.org/x/oauth2" | ||
|
||
"github.com/hatchet-dev/hatchet/api/v1/server/authn" | ||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" | ||
"github.com/hatchet-dev/hatchet/internal/config/server" | ||
"github.com/hatchet-dev/hatchet/internal/repository" | ||
"github.com/hatchet-dev/hatchet/internal/repository/prisma/db" | ||
) | ||
|
||
// Note: we want all errors to redirect, otherwise the user will be greeted with raw JSON in the middle of the login flow. | ||
func (u *UserService) UserUpdateGithubOauthCallback(ctx echo.Context, _ gen.UserUpdateGithubOauthCallbackRequestObject) (gen.UserUpdateGithubOauthCallbackResponseObject, error) { | ||
isValid, _, err := authn.NewSessionHelpers(u.config).ValidateOAuthState(ctx, "github") | ||
|
||
if err != nil || !isValid { | ||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, err, "Could not log in. Please try again and make sure cookies are enabled.") | ||
} | ||
|
||
token, err := u.config.Auth.GithubOAuthConfig.Exchange(context.Background(), ctx.Request().URL.Query().Get("code")) | ||
|
||
if err != nil { | ||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, err, "Forbidden") | ||
} | ||
|
||
if !token.Valid() { | ||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, fmt.Errorf("invalid token"), "Forbidden") | ||
} | ||
|
||
user, err := u.upsertGithubUserFromToken(u.config, token) | ||
|
||
if err != nil { | ||
if errors.Is(err, ErrGithubNotVerified) { | ||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, err, "Please verify your email on Github.") | ||
} | ||
|
||
if errors.Is(err, ErrGithubNoEmail) { | ||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, err, "Github user must have an email.") | ||
} | ||
|
||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, err, "Internal error.") | ||
} | ||
|
||
err = authn.NewSessionHelpers(u.config).SaveAuthenticated(ctx, user) | ||
|
||
if err != nil { | ||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, err, "Internal error.") | ||
} | ||
|
||
return gen.UserUpdateGithubOauthCallback302Response{ | ||
Headers: gen.UserUpdateGithubOauthCallback302ResponseHeaders{ | ||
Location: u.config.Runtime.ServerURL, | ||
}, | ||
}, nil | ||
} | ||
|
||
func (u *UserService) upsertGithubUserFromToken(config *server.ServerConfig, tok *oauth2.Token) (*db.UserModel, error) { | ||
gInfo, err := u.getGithubEmailFromToken(tok) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := u.checkUserRestrictionsForEmail(config, gInfo.Email); err != nil { | ||
return nil, err | ||
} | ||
|
||
expiresAt := tok.Expiry | ||
|
||
// use the encryption service to encrypt the access and refresh token | ||
accessTokenEncrypted, err := config.Encryption.Encrypt([]byte(tok.AccessToken), "github_access_token") | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to encrypt access token: %s", err.Error()) | ||
} | ||
|
||
refreshTokenEncrypted, err := config.Encryption.Encrypt([]byte(tok.RefreshToken), "github_refresh_token") | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to encrypt refresh token: %s", err.Error()) | ||
} | ||
|
||
oauthOpts := &repository.OAuthOpts{ | ||
Provider: "github", | ||
ProviderUserId: gInfo.ID, | ||
AccessToken: accessTokenEncrypted, | ||
RefreshToken: &refreshTokenEncrypted, | ||
ExpiresAt: &expiresAt, | ||
} | ||
|
||
user, err := u.config.APIRepository.User().GetUserByEmail(gInfo.Email) | ||
|
||
switch err { | ||
case nil: | ||
user, err = u.config.APIRepository.User().UpdateUser(user.ID, &repository.UpdateUserOpts{ | ||
EmailVerified: repository.BoolPtr(gInfo.EmailVerified), | ||
Name: repository.StringPtr(gInfo.Name), | ||
OAuth: oauthOpts, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to update user: %s", err.Error()) | ||
} | ||
case db.ErrNotFound: | ||
user, err = u.config.APIRepository.User().CreateUser(&repository.CreateUserOpts{ | ||
Email: gInfo.Email, | ||
EmailVerified: repository.BoolPtr(gInfo.EmailVerified), | ||
Name: repository.StringPtr(gInfo.Name), | ||
OAuth: oauthOpts, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to create user: %s", err.Error()) | ||
} | ||
default: | ||
return nil, fmt.Errorf("failed to get user: %s", err.Error()) | ||
} | ||
|
||
return user, nil | ||
} | ||
|
||
var ErrGithubNotVerified = fmt.Errorf("Please verify your email on Github") | ||
var ErrGithubNoEmail = fmt.Errorf("Github user must have an email") | ||
|
||
type githubInfo struct { | ||
Email string | ||
EmailVerified bool | ||
Name string | ||
ID string | ||
} | ||
|
||
func (u *UserService) getGithubEmailFromToken(tok *oauth2.Token) (*githubInfo, error) { | ||
client := githubsdk.NewClient(u.config.Auth.GithubOAuthConfig.Client(context.Background(), tok)) | ||
|
||
user, _, err := client.Users.Get(context.Background(), "") | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
emails, _, err := client.Users.ListEmails(context.Background(), &githubsdk.ListOptions{}) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
primary := "" | ||
verified := false | ||
|
||
// get the primary email | ||
for _, email := range emails { | ||
if email.GetPrimary() { | ||
primary = email.GetEmail() | ||
verified = email.GetVerified() | ||
break | ||
} | ||
} | ||
|
||
if primary == "" { | ||
return nil, ErrGithubNoEmail | ||
} | ||
|
||
if !verified { | ||
return nil, ErrGithubNotVerified | ||
} | ||
|
||
id := user.GetID() | ||
|
||
if id == 0 { | ||
return nil, fmt.Errorf("Github user has no ID") | ||
} | ||
|
||
return &githubInfo{ | ||
Email: primary, | ||
EmailVerified: verified, | ||
Name: user.GetName(), | ||
ID: fmt.Sprintf("%d", id), | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package users | ||
|
||
import ( | ||
"github.com/labstack/echo/v4" | ||
|
||
"github.com/hatchet-dev/hatchet/api/v1/server/authn" | ||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" | ||
) | ||
|
||
// Note: we want all errors to redirect, otherwise the user will be greeted with raw JSON in the middle of the login flow. | ||
func (u *UserService) UserUpdateGithubOauthStart(ctx echo.Context, _ gen.UserUpdateGithubOauthStartRequestObject) (gen.UserUpdateGithubOauthStartResponseObject, error) { | ||
state, err := authn.NewSessionHelpers(u.config).SaveOAuthState(ctx, "github") | ||
|
||
if err != nil { | ||
return nil, authn.GetRedirectWithError(ctx, u.config.Logger, err, "Could not get cookie. Please make sure cookies are enabled.") | ||
} | ||
|
||
url := u.config.Auth.GithubOAuthConfig.AuthCodeURL(state) | ||
|
||
return gen.UserUpdateGithubOauthStart302Response{ | ||
Headers: gen.UserUpdateGithubOauthStart302ResponseHeaders{ | ||
Location: url, | ||
}, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.