This works very well, just need to determine what routes will be protected and such. For now, a simple system is setup, with more to come. For now, this is a WIP and needs some light work. But auth is almost complete.
125 lines
4.5 KiB
Go
125 lines
4.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
|
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
|
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// NOTE: HOW THIS WORKS
|
|
//
|
|
// I need to store the refresh token along side the user in the DB.
|
|
// Create a "session token" (cookie) with an expiration and store that in the DB and the session.
|
|
// Send this session token along with all the requests (stored in the session) and when
|
|
// authorization is needed, use it.
|
|
// Once the expiration of MY token expires, prompt the user to log back in.
|
|
//
|
|
// So what is the point of the Google refresh token? Well, its not really useful right now, but if
|
|
// we need to perform Google actions, we can use it to get more access tokens, which are needed for
|
|
// the Google actions. Currently, I have no need for it, but it will be stored anyway.
|
|
//
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
// AuthService implements the domain.AuthService defined in the domain module.
|
|
type AuthService struct {
|
|
userRepository domain.UserRepository
|
|
jwtSecret []byte
|
|
}
|
|
|
|
// Compile-time check to ensure the AuthService implements domain.AuthService
|
|
var _ domainAuth.AuthService = (*AuthService)(nil)
|
|
|
|
// NewAuthService creates a user service object which can be passed into the context. The service
|
|
// requires a user repository which it will use to hit the database when needed.
|
|
func NewAuthService(userRepository domain.UserRepository, jwtSecret []byte) domainAuth.AuthService {
|
|
return &AuthService{
|
|
userRepository: userRepository,
|
|
jwtSecret: jwtSecret,
|
|
}
|
|
}
|
|
|
|
// GetGoogleAuthUrl generates a URL which is used to redirect the user to the Google sign in page.
|
|
// This URL is in the Google domain and is not coupled with this application. The data from this URL
|
|
// will be passed into a callback and work to complete the Google OAuth workflow.
|
|
func (s *AuthService) GetGoogleAuthUrl() string {
|
|
url := auth.GoogleAuthConfig.AuthCodeURL(
|
|
"randomstate",
|
|
oauth2.AccessTypeOffline,
|
|
oauth2.ApprovalForce,
|
|
)
|
|
|
|
return url
|
|
}
|
|
|
|
// GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users
|
|
// data. The data is then used to log the user in or create an account.
|
|
func (s *AuthService) GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error) {
|
|
// Ensure the state matches, prevents M.I.T.M. attacks
|
|
if state != "randomstate" {
|
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
|
}
|
|
|
|
// Get access token from Google
|
|
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
|
if err != nil {
|
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
|
|
}
|
|
|
|
// Use the access token to get user data
|
|
googleUserInfo, err := auth.GetUserData(token.AccessToken)
|
|
if err != nil {
|
|
return "", domain.User{}, domain.GoogleUserInfo{}, err
|
|
}
|
|
|
|
// Attempt to get the user, user is nil when they don't exit
|
|
user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id)
|
|
if err != nil {
|
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Failed to get db user: %s", err)
|
|
}
|
|
|
|
// A user was found
|
|
if user != nil {
|
|
jwt, err := generateJwt(user.Id, user.Email, s.jwtSecret)
|
|
return jwt, *user, googleUserInfo, err
|
|
}
|
|
|
|
// user did not exist, need to create one
|
|
newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken)
|
|
if err != nil {
|
|
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Repository failed to create user: %s", err.Error())
|
|
}
|
|
|
|
jwt, err := generateJwt(newUser.Id, newUser.Email, s.jwtSecret)
|
|
return jwt, newUser, googleUserInfo, err
|
|
|
|
}
|
|
|
|
func generateJwt(userId int, email string, jwtSecret []byte) (string, error) {
|
|
expiration := time.Now().Add(7 * 24 * time.Hour)
|
|
|
|
claims := &domainServer.JwtClaims{
|
|
UserId: userId,
|
|
Email: email,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(expiration),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
Subject: fmt.Sprintf("%d", userId),
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenString, err := token.SignedString(jwtSecret)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Failed to sign jwt: %s", err.Error())
|
|
}
|
|
|
|
return tokenString, nil
|
|
}
|