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, error) { // Ensure the state matches, prevents M.I.T.M. attacks if state != "randomstate" { return "", 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 "", 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 "", 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 "", 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, err } // user did not exist, need to create one newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken) if err != nil { return "", fmt.Errorf("Repository failed to create user: %s", err.Error()) } jwt, err := generateJwt(newUser.Id, newUser.Email, s.jwtSecret) return jwt, err } // generateJwt requires user data and returns a JSON web token which can be stored in the browsers // cookies. This token is used to log a user into the application and allow access to protected // routes. 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 }