chore: move the go code into a subfolder

This commit is contained in:
Florian Forster
2025-08-05 15:20:32 -07:00
parent 4ad22ba456
commit cd2921de26
2978 changed files with 373 additions and 300 deletions

View File

@@ -0,0 +1,190 @@
package github
import (
"strconv"
"time"
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
const (
authURL = "https://github.com/login/oauth/authorize"
tokenURL = "https://github.com/login/oauth/access_token"
profileURL = "https://api.github.com/user"
name = "GitHub"
)
var _ idp.Provider = (*Provider)(nil)
// New creates a GitHub.com provider using the [oauth.Provider] (OAuth 2.0 generic provider)
func New(clientID, secret, callbackURL string, scopes []string, options ...oauth.ProviderOpts) (*Provider, error) {
return NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL, scopes, options...)
}
// NewCustomURL creates a GitHub provider using the [oauth.Provider] (OAuth 2.0 generic provider)
// with custom endpoints, e.g. GitHub Enterprise server
func NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL string, scopes []string, options ...oauth.ProviderOpts) (*Provider, error) {
rp, err := oauth.New(
newConfig(clientID, secret, callbackURL, authURL, tokenURL, scopes),
name,
profileURL,
func() idp.User {
return new(User)
},
options...,
)
if err != nil {
return nil, err
}
return &Provider{
Provider: rp,
}, nil
}
// Provider is the [idp.Provider] implementation for GitHub
type Provider struct {
*oauth.Provider
}
func newConfig(clientID, secret, callbackURL, authURL, tokenURL string, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: clientID,
ClientSecret: secret,
RedirectURL: callbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: scopes,
}
return c
}
// User is a representation of the authenticated GitHub user and implements the [idp.User] interface
// https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
type User struct {
Login string `json:"login"`
ID int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
Name string `json:"name"`
Company string `json:"company"`
Blog string `json:"blog"`
Location string `json:"location"`
Email domain.EmailAddress `json:"email"`
Hireable bool `json:"hireable"`
Bio string `json:"bio"`
TwitterUsername string `json:"twitter_username"`
PublicRepos int `json:"public_repos"`
PublicGists int `json:"public_gists"`
Followers int `json:"followers"`
Following int `json:"following"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PrivateGists int `json:"private_gists"`
TotalPrivateRepos int `json:"total_private_repos"`
OwnedPrivateRepos int `json:"owned_private_repos"`
DiskUsage int `json:"disk_usage"`
Collaborators int `json:"collaborators"`
TwoFactorAuthentication bool `json:"two_factor_authentication"`
Plan struct {
Name string `json:"name"`
Space int `json:"space"`
PrivateRepos int `json:"private_repos"`
Collaborators int `json:"collaborators"`
} `json:"plan"`
}
// GetID is an implementation of the [idp.User] interface.
func (u *User) GetID() string {
return strconv.Itoa(u.ID)
}
// GetFirstName is an implementation of the [idp.User] interface.
// It returns an empty string because GitHub does not provide the user's firstname.
func (u *User) GetFirstName() string {
return ""
}
// GetLastName is an implementation of the [idp.User] interface.
// It returns an empty string because GitHub does not provide the user's lastname.
func (u *User) GetLastName() string {
// GitHub does not provide the user's lastname
return ""
}
// GetDisplayName is an implementation of the [idp.User] interface.
func (u *User) GetDisplayName() string {
return u.Name
}
// GetNickname is an implementation of the [idp.User] interface
// returning the login name of the GitHub user.
func (u *User) GetNickname() string {
return u.Login
}
// GetPreferredUsername is an implementation of the [idp.User] interface
// returning the login name of the GitHub user.
func (u *User) GetPreferredUsername() string {
return u.Login
}
// GetEmail is an implementation of the [idp.User] interface.
func (u *User) GetEmail() domain.EmailAddress {
return u.Email
}
// IsEmailVerified is an implementation of the [idp.User] interface.
// It returns true because GitHub validates emails themselves.
func (u *User) IsEmailVerified() bool {
return true
}
// GetPhone is an implementation of the [idp.User] interface.
// It returns an empty string because GitHub does not provide the user's phone.
func (u *User) GetPhone() domain.PhoneNumber {
return ""
}
// IsPhoneVerified is an implementation of the [idp.User] interface
// it returns false because GitHub does not provide the user's phone
func (u *User) IsPhoneVerified() bool {
return false
}
// GetPreferredLanguage is an implementation of the [idp.User] interface.
// It returns [language.Und] because GitHub does not provide the user's language.
func (u *User) GetPreferredLanguage() language.Tag {
return language.Und
}
// GetProfile is an implementation of the [idp.User] interface.
func (u *User) GetProfile() string {
return u.HtmlUrl
}
// GetAvatarURL is an implementation of the [idp.User] interface.
func (u *User) GetAvatarURL() string {
return u.AvatarUrl
}

View File

@@ -0,0 +1,57 @@
package github
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
func TestProvider_BeginAuth(t *testing.T) {
type fields struct {
clientID string
clientSecret string
redirectURI string
scopes []string
options []oauth.ProviderOpts
}
tests := []struct {
name string
fields fields
want idp.Session
}{
{
name: "successful auth",
fields: fields{
clientID: "clientID",
clientSecret: "clientSecret",
redirectURI: "redirectURI",
},
want: &oauth.Session{
AuthURL: "https://github.com/login/oauth/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&state=testState",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
r.NoError(err)
ctx := context.Background()
session, err := provider.BeginAuth(ctx, "testState")
r.NoError(err)
wantAuth, wantErr := tt.want.GetAuth(ctx)
gotAuth, gotErr := session.GetAuth(ctx)
a.Equal(wantAuth, gotAuth)
a.ErrorIs(gotErr, wantErr)
})
}
}

View File

@@ -0,0 +1,216 @@
package github
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
func TestSession_FetchUser(t *testing.T) {
type fields struct {
clientID string
clientSecret string
redirectURI string
httpMock func()
authURL string
code string
tokens *oidc.Tokens[*oidc.IDTokenClaims]
scopes []string
options []oauth.ProviderOpts
}
type args struct {
session idp.Session
}
type want struct {
err func(error) bool
user idp.User
id string
firstName string
lastName string
displayName string
nickName string
preferredUsername string
email string
isEmailVerified bool
phone string
isPhoneVerified bool
preferredLanguage language.Tag
avatarURL string
profile string
}
tests := []struct {
name string
fields fields
args args
want want
}{
{
name: "unauthenticated session, error",
fields: fields{
clientID: "clientID",
clientSecret: "clientSecret",
redirectURI: "redirectURI",
httpMock: func() {
gock.New("https://api.github.com").
Get("/user").
Reply(200).
JSON(userinfo())
},
authURL: "https://github.com/login/oauth/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&state=testState",
tokens: nil,
},
args: args{
&oauth.Session{},
},
want: want{
err: func(err error) bool {
return errors.Is(err, oauth.ErrCodeMissing)
},
},
},
{
name: "user error",
fields: fields{
clientID: "clientID",
clientSecret: "clientSecret",
redirectURI: "redirectURI",
httpMock: func() {
gock.New("https://api.github.com").
Get("/user").
Reply(http.StatusInternalServerError)
},
authURL: "https://github.com/login/oauth/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&state=testState",
tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
TokenType: oidc.BearerToken,
},
},
},
args: args{
&oauth.Session{},
},
want: want{
err: func(err error) bool {
return err.Error() == "http status not ok: 500 Internal Server Error "
},
},
},
{
name: "successful fetch",
fields: fields{
clientID: "clientID",
clientSecret: "clientSecret",
redirectURI: "redirectURI",
httpMock: func() {
gock.New("https://api.github.com").
Get("/user").
Reply(200).
JSON(userinfo())
},
authURL: "https://github.com/login/oauth/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&state=testState",
tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
TokenType: oidc.BearerToken,
},
},
},
args: args{
&oauth.Session{},
},
want: want{
user: &User{
Login: "login",
ID: 1,
AvatarUrl: "avatarURL",
GravatarId: "gravatarID",
Name: "name",
Email: "email",
HtmlUrl: "htmlURL",
CreatedAt: time.Date(2023, 01, 10, 11, 10, 35, 0, time.UTC),
UpdatedAt: time.Date(2023, 01, 10, 11, 10, 35, 0, time.UTC),
},
id: "1",
firstName: "",
lastName: "",
displayName: "name",
nickName: "login",
preferredUsername: "login",
email: "email",
isEmailVerified: true,
phone: "",
isPhoneVerified: false,
preferredLanguage: language.Und,
avatarURL: "avatarURL",
profile: "htmlURL",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer gock.Off()
tt.fields.httpMock()
a := assert.New(t)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
require.NoError(t, err)
session := &oauth.Session{
AuthURL: tt.fields.authURL,
Code: tt.fields.code,
Tokens: tt.fields.tokens,
Provider: provider.Provider,
}
user, err := session.FetchUser(context.Background())
if tt.want.err != nil && !tt.want.err(err) {
a.Fail("invalid error", err)
}
if tt.want.err == nil {
a.NoError(err)
a.Equal(tt.want.user, user)
a.Equal(tt.want.id, user.GetID())
a.Equal(tt.want.firstName, user.GetFirstName())
a.Equal(tt.want.lastName, user.GetLastName())
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())
a.Equal(tt.want.profile, user.GetProfile())
}
})
}
}
func userinfo() *User {
return &User{
Login: "login",
ID: 1,
AvatarUrl: "avatarURL",
GravatarId: "gravatarID",
Name: "name",
Email: "email",
HtmlUrl: "htmlURL",
CreatedAt: time.Date(2023, 01, 10, 11, 10, 35, 0, time.UTC),
UpdatedAt: time.Date(2023, 01, 10, 11, 10, 35, 0, time.UTC),
}
}