package service import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "time" "tinyauth/internal/config" "golang.org/x/oauth2" "golang.org/x/oauth2/endpoints" ) var GithubOAuthScopes = []string{"user:email", "read:user"} type GithubEmailResponse []struct { Email string `json:"email"` Primary bool `json:"primary"` } type GithubUserInfoResponse struct { Login string `json:"login"` Name string `json:"name"` } type GithubOAuthService struct { config oauth2.Config context context.Context token *oauth2.Token verifier string name string } func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService { return &GithubOAuthService{ config: oauth2.Config{ ClientID: config.ClientID, ClientSecret: config.ClientSecret, RedirectURL: config.RedirectURL, Scopes: GithubOAuthScopes, Endpoint: endpoints.GitHub, }, name: config.Name, } } func (github *GithubOAuthService) Init() error { httpClient := &http.Client{} ctx := context.Background() ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) github.context = ctx return nil } func (github *GithubOAuthService) GenerateState() string { b := make([]byte, 128) _, err := rand.Read(b) if err != nil { return base64.RawURLEncoding.EncodeToString(fmt.Appendf(nil, "state-%d", time.Now().UnixNano())) } state := base64.RawURLEncoding.EncodeToString(b) return state } func (github *GithubOAuthService) GenerateVerifier() string { verifier := oauth2.GenerateVerifier() github.verifier = verifier return verifier } func (github *GithubOAuthService) GetAuthURL(state string) string { return github.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(github.verifier)) } func (github *GithubOAuthService) VerifyCode(code string) error { token, err := github.config.Exchange(github.context, code, oauth2.VerifierOption(github.verifier)) if err != nil { return err } github.token = token return nil } func (github *GithubOAuthService) Userinfo() (config.Claims, error) { var user config.Claims client := github.config.Client(github.context, github.token) req, err := http.NewRequest("GET", "https://api.github.com/user", nil) if err != nil { return user, err } req.Header.Set("Accept", "application/vnd.github+json") res, err := client.Do(req) if err != nil { return user, err } defer res.Body.Close() if res.StatusCode < 200 || res.StatusCode >= 300 { return user, fmt.Errorf("request failed with status: %s", res.Status) } body, err := io.ReadAll(res.Body) if err != nil { return user, err } var userInfo GithubUserInfoResponse err = json.Unmarshal(body, &userInfo) if err != nil { return user, err } req, err = http.NewRequest("GET", "https://api.github.com/user/emails", nil) if err != nil { return user, err } req.Header.Set("Accept", "application/vnd.github+json") res, err = client.Do(req) if err != nil { return user, err } defer res.Body.Close() if res.StatusCode < 200 || res.StatusCode >= 300 { return user, fmt.Errorf("request failed with status: %s", res.Status) } body, err = io.ReadAll(res.Body) if err != nil { return user, err } var emails GithubEmailResponse err = json.Unmarshal(body, &emails) if err != nil { return user, err } for _, email := range emails { if email.Primary { user.Email = email.Email break } } if len(emails) == 0 { return user, errors.New("no emails found") } // Use first available email if no primary email was found if user.Email == "" { user.Email = emails[0].Email } user.PreferredUsername = userInfo.Login user.Name = userInfo.Name return user, nil } func (github *GithubOAuthService) GetName() string { return github.name }