mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-03-24 15:37:53 +00:00
wip
This commit is contained in:
@@ -1,132 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GenericOAuthService struct {
|
|
||||||
config oauth2.Config
|
|
||||||
context context.Context
|
|
||||||
token *oauth2.Token
|
|
||||||
verifier string
|
|
||||||
insecureSkipVerify bool
|
|
||||||
userinfoUrl string
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthService {
|
|
||||||
return &GenericOAuthService{
|
|
||||||
config: oauth2.Config{
|
|
||||||
ClientID: config.ClientID,
|
|
||||||
ClientSecret: config.ClientSecret,
|
|
||||||
RedirectURL: config.RedirectURL,
|
|
||||||
Scopes: config.Scopes,
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: config.AuthURL,
|
|
||||||
TokenURL: config.TokenURL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
insecureSkipVerify: config.Insecure,
|
|
||||||
userinfoUrl: config.UserinfoURL,
|
|
||||||
name: config.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (generic *GenericOAuthService) Init() error {
|
|
||||||
transport := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: generic.insecureSkipVerify,
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
|
||||||
|
|
||||||
generic.context = ctx
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (generic *GenericOAuthService) 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 (generic *GenericOAuthService) GenerateVerifier() string {
|
|
||||||
verifier := oauth2.GenerateVerifier()
|
|
||||||
generic.verifier = verifier
|
|
||||||
return verifier
|
|
||||||
}
|
|
||||||
|
|
||||||
func (generic *GenericOAuthService) GetAuthURL(state string) string {
|
|
||||||
return generic.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(generic.verifier))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (generic *GenericOAuthService) VerifyCode(code string) error {
|
|
||||||
token, err := generic.config.Exchange(generic.context, code, oauth2.VerifierOption(generic.verifier))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
generic.token = token
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
|
|
||||||
var user config.Claims
|
|
||||||
|
|
||||||
client := generic.config.Client(generic.context, generic.token)
|
|
||||||
|
|
||||||
res, err := client.Get(generic.userinfoUrl)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
tlog.App.Trace().Str("body", string(body)).Msg("Userinfo response body")
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &user)
|
|
||||||
if err != nil {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (generic *GenericOAuthService) GetName() string {
|
|
||||||
return generic.name
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/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"`
|
|
||||||
ID int `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
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
|
|
||||||
user.Sub = strconv.Itoa(userInfo.ID)
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (github *GithubOAuthService) GetName() string {
|
|
||||||
return github.name
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/endpoints"
|
|
||||||
)
|
|
||||||
|
|
||||||
var GoogleOAuthScopes = []string{"openid", "email", "profile"}
|
|
||||||
|
|
||||||
type GoogleOAuthService struct {
|
|
||||||
config oauth2.Config
|
|
||||||
context context.Context
|
|
||||||
token *oauth2.Token
|
|
||||||
verifier string
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService {
|
|
||||||
return &GoogleOAuthService{
|
|
||||||
config: oauth2.Config{
|
|
||||||
ClientID: config.ClientID,
|
|
||||||
ClientSecret: config.ClientSecret,
|
|
||||||
RedirectURL: config.RedirectURL,
|
|
||||||
Scopes: GoogleOAuthScopes,
|
|
||||||
Endpoint: endpoints.Google,
|
|
||||||
},
|
|
||||||
name: config.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (google *GoogleOAuthService) Init() error {
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
|
||||||
google.context = ctx
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (oauth *GoogleOAuthService) 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 (google *GoogleOAuthService) GenerateVerifier() string {
|
|
||||||
verifier := oauth2.GenerateVerifier()
|
|
||||||
google.verifier = verifier
|
|
||||||
return verifier
|
|
||||||
}
|
|
||||||
|
|
||||||
func (google *GoogleOAuthService) GetAuthURL(state string) string {
|
|
||||||
return google.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(google.verifier))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (google *GoogleOAuthService) VerifyCode(code string) error {
|
|
||||||
token, err := google.config.Exchange(google.context, code, oauth2.VerifierOption(google.verifier))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
google.token = token
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (google *GoogleOAuthService) Userinfo() (config.Claims, error) {
|
|
||||||
var user config.Claims
|
|
||||||
|
|
||||||
client := google.config.Client(google.context, google.token)
|
|
||||||
|
|
||||||
res, err := client.Get("https://openidconnect.googleapis.com/v1/userinfo")
|
|
||||||
if err != nil {
|
|
||||||
return config.Claims{}, 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 config.Claims{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &user)
|
|
||||||
if err != nil {
|
|
||||||
return config.Claims{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user.PreferredUsername = strings.SplitN(user.Email, "@", 2)[0]
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (google *GoogleOAuthService) GetName() string {
|
|
||||||
return google.name
|
|
||||||
}
|
|
||||||
@@ -1,60 +1,48 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuthService interface {
|
type OAuthServiceImpl interface {
|
||||||
Init() error
|
Name() string
|
||||||
GenerateState() string
|
NewRandom() string
|
||||||
GenerateVerifier() string
|
GetAuthURL(state string, verifier string) string
|
||||||
GetAuthURL(state string) string
|
GetToken(code string, verifier string) (*oauth2.Token, error)
|
||||||
VerifyCode(code string) error
|
GetUserinfo(token *oauth2.Token) (config.Claims, error)
|
||||||
Userinfo() (config.Claims, error)
|
|
||||||
GetName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthBrokerService struct {
|
type OAuthBrokerService struct {
|
||||||
services map[string]OAuthService
|
services map[string]OAuthServiceImpl
|
||||||
configs map[string]config.OAuthServiceConfig
|
configs map[string]config.OAuthServiceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var presets = map[string]func(config config.OAuthServiceConfig) *OAuthService{
|
||||||
|
"github": newGitHubOAuthService,
|
||||||
|
"google": newGoogleOAuthService,
|
||||||
|
}
|
||||||
|
|
||||||
func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService {
|
func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService {
|
||||||
return &OAuthBrokerService{
|
return &OAuthBrokerService{
|
||||||
services: make(map[string]OAuthService),
|
services: make(map[string]OAuthServiceImpl),
|
||||||
configs: configs,
|
configs: configs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (broker *OAuthBrokerService) Init() error {
|
func (broker *OAuthBrokerService) Init() error {
|
||||||
for name, cfg := range broker.configs {
|
for name, cfg := range broker.configs {
|
||||||
switch name {
|
if presetFunc, exists := presets[name]; exists {
|
||||||
case "github":
|
broker.services[name] = presetFunc(cfg)
|
||||||
service := NewGithubOAuthService(cfg)
|
tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset")
|
||||||
broker.services[name] = service
|
} else {
|
||||||
case "google":
|
broker.services[name] = NewOAuthService(cfg)
|
||||||
service := NewGoogleOAuthService(cfg)
|
tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from config")
|
||||||
broker.services[name] = service
|
|
||||||
default:
|
|
||||||
service := NewGenericOAuthService(cfg)
|
|
||||||
broker.services[name] = service
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, service := range broker.services {
|
|
||||||
err := service.Init()
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Error().Err(err).Msgf("Failed to initialize OAuth service: %s", name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tlog.App.Info().Str("service", name).Msg("Initialized OAuth service")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,15 +55,7 @@ func (broker *OAuthBrokerService) GetConfiguredServices() []string {
|
|||||||
return services
|
return services
|
||||||
}
|
}
|
||||||
|
|
||||||
func (broker *OAuthBrokerService) GetService(name string) (OAuthService, bool) {
|
func (broker *OAuthBrokerService) GetService(name string) (OAuthServiceImpl, bool) {
|
||||||
service, exists := broker.services[name]
|
service, exists := broker.services[name]
|
||||||
return service, exists
|
return service, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
func (broker *OAuthBrokerService) GetUser(service string) (config.Claims, error) {
|
|
||||||
oauthService, exists := broker.services[service]
|
|
||||||
if !exists {
|
|
||||||
return config.Claims{}, errors.New("oauth service not found")
|
|
||||||
}
|
|
||||||
return oauthService.Userinfo()
|
|
||||||
}
|
|
||||||
|
|||||||
121
internal/service/oauth_extractors.go
Normal file
121
internal/service/oauth_extractors.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GithubEmailResponse []struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GithubUserInfoResponse struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultExtractor(client *http.Client, url string) (config.Claims, error) {
|
||||||
|
var claims config.Claims
|
||||||
|
|
||||||
|
res, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return config.Claims{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
|
return config.Claims{}, fmt.Errorf("request failed with status: %s", res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return config.Claims{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Trace().Str("body", string(body)).Msg("Userinfo response body")
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &claims)
|
||||||
|
if err != nil {
|
||||||
|
return config.Claims{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func githubExtractor(client *http.Client, url string) (config.Claims, error) {
|
||||||
|
var user config.Claims
|
||||||
|
|
||||||
|
userInfo, err := githubRequest[GithubUserInfoResponse](client, "https://api.github.com/user")
|
||||||
|
if err != nil {
|
||||||
|
return config.Claims{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userEmails, err := githubRequest[GithubEmailResponse](client, "https://api.github.com/user/emails")
|
||||||
|
if err != nil {
|
||||||
|
return config.Claims{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userEmails) == 0 {
|
||||||
|
return user, errors.New("no emails found")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, email := range userEmails {
|
||||||
|
if email.Primary {
|
||||||
|
user.Email = email.Email
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use first available email if no primary email was found
|
||||||
|
if user.Email == "" {
|
||||||
|
user.Email = userEmails[0].Email
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PreferredUsername = userInfo.Login
|
||||||
|
user.Name = userInfo.Name
|
||||||
|
user.Sub = strconv.Itoa(userInfo.ID)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func githubRequest[T any](client *http.Client, url string) (T, error) {
|
||||||
|
var githubRes T
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
|
||||||
|
if err != nil {
|
||||||
|
return githubRes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return githubRes, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
|
return githubRes, fmt.Errorf("request failed with status: %s", res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return githubRes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &githubRes)
|
||||||
|
if err != nil {
|
||||||
|
return githubRes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return githubRes, nil
|
||||||
|
}
|
||||||
23
internal/service/oauth_presets.go
Normal file
23
internal/service/oauth_presets.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"golang.org/x/oauth2/endpoints"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||||
|
scopes := []string{"openid", "email", "profile"}
|
||||||
|
config.Scopes = scopes
|
||||||
|
config.AuthURL = endpoints.Google.AuthURL
|
||||||
|
config.TokenURL = endpoints.Google.TokenURL
|
||||||
|
config.UserinfoURL = "https://openidconnect.googleapis.com/v1/userinfo"
|
||||||
|
return NewOAuthService(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||||
|
scopes := []string{"read:user", "user:email"}
|
||||||
|
config.Scopes = scopes
|
||||||
|
config.AuthURL = endpoints.GitHub.AuthURL
|
||||||
|
config.TokenURL = endpoints.GitHub.TokenURL
|
||||||
|
return NewOAuthService(config).WithUserinfoExtractor(githubExtractor)
|
||||||
|
}
|
||||||
78
internal/service/oauth_service.go
Normal file
78
internal/service/oauth_service.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserinfoExtractor func(client *http.Client, url string) (config.Claims, error)
|
||||||
|
|
||||||
|
type OAuthService struct {
|
||||||
|
serviceCfg config.OAuthServiceConfig
|
||||||
|
config *oauth2.Config
|
||||||
|
ctx context.Context
|
||||||
|
userinfoExtractor UserinfoExtractor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: config.Insecure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||||
|
|
||||||
|
return &OAuthService{
|
||||||
|
serviceCfg: config,
|
||||||
|
config: &oauth2.Config{
|
||||||
|
ClientID: config.ClientID,
|
||||||
|
ClientSecret: config.ClientSecret,
|
||||||
|
RedirectURL: config.RedirectURL,
|
||||||
|
Scopes: config.Scopes,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: config.AuthURL,
|
||||||
|
TokenURL: config.TokenURL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx: ctx,
|
||||||
|
userinfoExtractor: defaultExtractor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) WithUserinfoExtractor(extractor UserinfoExtractor) *OAuthService {
|
||||||
|
s.userinfoExtractor = extractor
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) Name() string {
|
||||||
|
return s.serviceCfg.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) NewRandom() string {
|
||||||
|
// The generate verifier function just creates a random string,
|
||||||
|
// so we can use it to generate a random state as well
|
||||||
|
random := oauth2.GenerateVerifier()
|
||||||
|
return random
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) GetAuthURL(state string, verifier string) string {
|
||||||
|
return s.config.AuthCodeURL(state, oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) GetToken(code string, verifier string) (*oauth2.Token, error) {
|
||||||
|
return s.config.Exchange(s.ctx, code, oauth2.VerifierOption(verifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (config.Claims, error) {
|
||||||
|
client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
|
||||||
|
return s.userinfoExtractor(client, s.serviceCfg.UserinfoURL)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user