diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f2c915..566a3118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.17.0-WIP + +- Added Instagram OAuth2 ([#2534](https://github.com/pocketbase/pocketbase/pull/2534); thanks @pnmcosta). + + ## v0.16.2-WIP - Fixed backups archive not excluding the local `backups` dir on Windows ([#2548](https://github.com/pocketbase/pocketbase/discussions/2548#discussioncomment-5979712)). diff --git a/apis/settings_test.go b/apis/settings_test.go index 314e48d9..eb858690 100644 --- a/apis/settings_test.go +++ b/apis/settings_test.go @@ -75,6 +75,7 @@ func TestSettingsList(t *testing.T) { `"oidc2Auth":{`, `"oidc3Auth":{`, `"appleAuth":{`, + `"instagramAuth":{`, `"secret":"******"`, `"clientSecret":"******"`, }, @@ -153,6 +154,7 @@ func TestSettingsSet(t *testing.T) { `"oidc2Auth":{`, `"oidc3Auth":{`, `"appleAuth":{`, + `"instagramAuth":{`, `"secret":"******"`, `"clientSecret":"******"`, `"appName":"acme_test"`, @@ -220,6 +222,7 @@ func TestSettingsSet(t *testing.T) { `"oidc2Auth":{`, `"oidc3Auth":{`, `"appleAuth":{`, + `"instagramAuth":{`, `"secret":"******"`, `"clientSecret":"******"`, `"appName":"update_test"`, diff --git a/models/settings/settings.go b/models/settings/settings.go index 409824d7..8da7ee09 100644 --- a/models/settings/settings.go +++ b/models/settings/settings.go @@ -60,6 +60,7 @@ type Settings struct { OIDC2Auth AuthProviderConfig `form:"oidc2Auth" json:"oidc2Auth"` OIDC3Auth AuthProviderConfig `form:"oidc3Auth" json:"oidc3Auth"` AppleAuth AuthProviderConfig `form:"appleAuth" json:"appleAuth"` + InstagramAuth AuthProviderConfig `form:"instagramAuth" json:"instagramAuth"` } // New creates and returns a new default Settings instance. @@ -175,6 +176,9 @@ func New() *Settings { AppleAuth: AuthProviderConfig{ Enabled: false, }, + InstagramAuth: AuthProviderConfig{ + Enabled: false, + }, } } @@ -215,6 +219,7 @@ func (s *Settings) Validate() error { validation.Field(&s.OIDC2Auth), validation.Field(&s.OIDC3Auth), validation.Field(&s.AppleAuth), + validation.Field(&s.InstagramAuth), ) } @@ -278,6 +283,7 @@ func (s *Settings) RedactClone() (*Settings, error) { &clone.OIDC2Auth.ClientSecret, &clone.OIDC3Auth.ClientSecret, &clone.AppleAuth.ClientSecret, + &clone.InstagramAuth.ClientSecret, } // mask all sensitive fields @@ -315,6 +321,7 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig { auth.NameOIDC + "2": s.OIDC2Auth, auth.NameOIDC + "3": s.OIDC3Auth, auth.NameApple: s.AppleAuth, + auth.NameInstagram: s.InstagramAuth, } } diff --git a/models/settings/settings_test.go b/models/settings/settings_test.go index 1e268017..c182ef87 100644 --- a/models/settings/settings_test.go +++ b/models/settings/settings_test.go @@ -67,6 +67,8 @@ func TestSettingsValidate(t *testing.T) { s.OIDC3Auth.ClientId = "" s.AppleAuth.Enabled = true s.AppleAuth.ClientId = "" + s.InstagramAuth.Enabled = true + s.InstagramAuth.ClientId = "" // check if Validate() is triggering the members validate methods. err := s.Validate() @@ -105,6 +107,7 @@ func TestSettingsValidate(t *testing.T) { `"oidc2Auth":{`, `"oidc3Auth":{`, `"appleAuth":{`, + `"instagramAuth":{`, } errBytes, _ := json.Marshal(err) @@ -172,6 +175,8 @@ func TestSettingsMerge(t *testing.T) { s2.OIDC3Auth.ClientId = "oidc3_test" s2.AppleAuth.Enabled = true s2.AppleAuth.ClientId = "apple_test" + s2.InstagramAuth.Enabled = true + s2.InstagramAuth.ClientId = "instagram_test" if err := s1.Merge(s2); err != nil { t.Fatal(err) @@ -259,6 +264,7 @@ func TestSettingsRedactClone(t *testing.T) { s1.OIDC2Auth.ClientSecret = testSecret s1.OIDC3Auth.ClientSecret = testSecret s1.AppleAuth.ClientSecret = testSecret + s1.InstagramAuth.ClientSecret = testSecret s1Bytes, err := json.Marshal(s1) if err != nil { @@ -314,6 +320,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) { s.OIDC2Auth.ClientId = "oidc2_test" s.OIDC3Auth.ClientId = "oidc3_test" s.AppleAuth.ClientId = "apple_test" + s.InstagramAuth.ClientId = "instagram_test" result := s.NamedAuthProviderConfigs() @@ -342,6 +349,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) { `"oidc2":{"enabled":false,"clientId":"oidc2_test"`, `"oidc3":{"enabled":false,"clientId":"oidc3_test"`, `"apple":{"enabled":false,"clientId":"apple_test"`, + `"instagram":{"enabled":false,"clientId":"instagram_test"`, } for _, p := range expectedParts { if !strings.Contains(encodedStr, p) { diff --git a/tools/auth/auth.go b/tools/auth/auth.go index 97a9fc93..a225ca6e 100644 --- a/tools/auth/auth.go +++ b/tools/auth/auth.go @@ -129,6 +129,8 @@ func NewProviderByName(name string) (Provider, error) { return NewOIDCProvider(), nil case NameApple: return NewAppleProvider(), nil + case NameInstagram: + return NewInstagramProvider(), nil default: return nil, errors.New("Missing provider " + name) } diff --git a/tools/auth/auth_test.go b/tools/auth/auth_test.go index 54da3fb4..0d264843 100644 --- a/tools/auth/auth_test.go +++ b/tools/auth/auth_test.go @@ -180,4 +180,13 @@ func TestNewProviderByName(t *testing.T) { if _, ok := p.(*auth.Apple); !ok { t.Error("Expected to be instance of *auth.Apple") } + + // instagram + p, err = auth.NewProviderByName(auth.NameInstagram) + if err != nil { + t.Errorf("Expected nil, got error %v", err) + } + if _, ok := p.(*auth.Instagram); !ok { + t.Error("Expected to be instance of *auth.Instagram") + } } diff --git a/tools/auth/instagram.go b/tools/auth/instagram.go new file mode 100644 index 00000000..db2f60aa --- /dev/null +++ b/tools/auth/instagram.go @@ -0,0 +1,63 @@ +package auth + +import ( + "context" + "encoding/json" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/instagram" +) + +var _ Provider = (*Instagram)(nil) + +// NameInstagram is the unique name of the Instagram provider. +const NameInstagram string = "instagram" + +// Instagram allows authentication via Instagram OAuth2. +type Instagram struct { + *baseProvider +} + +// NewInstagramProvider creates new Instagram provider instance with some defaults. +func NewInstagramProvider() *Instagram { + return &Instagram{&baseProvider{ + ctx: context.Background(), + scopes: []string{"user_profile"}, + authUrl: instagram.Endpoint.AuthURL, + tokenUrl: instagram.Endpoint.TokenURL, + userApiUrl: "https://graph.instagram.com/me?fields=id,username,account_type", + }} +} + +// FetchAuthUser returns an AuthUser instance based on the Instagram's user api. +// +// API reference: https://developers.facebook.com/docs/instagram-basic-display-api/reference/user#fields +func (p *Instagram) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { + data, err := p.FetchRawUserData(token) + if err != nil { + return nil, err + } + + rawUser := map[string]any{} + if err := json.Unmarshal(data, &rawUser); err != nil { + return nil, err + } + + extracted := struct { + Id string `json:"id"` + Username string `json:"username"` + }{} + if err := json.Unmarshal(data, &extracted); err != nil { + return nil, err + } + + user := &AuthUser{ + Id: extracted.Id, + Username: extracted.Username, + RawUser: rawUser, + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + } + + return user, nil +} diff --git a/ui/public/images/oauth2/instagram.svg b/ui/public/images/oauth2/instagram.svg new file mode 100644 index 00000000..e3c21c84 --- /dev/null +++ b/ui/public/images/oauth2/instagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/providers.js b/ui/src/providers.js index a7742a4f..5e384001 100644 --- a/ui/src/providers.js +++ b/ui/src/providers.js @@ -25,16 +25,21 @@ export default [ title: "Google", logo: "google.svg", }, + { + key: "microsoftAuth", + title: "Microsoft", + logo: "microsoft.svg", + optionsComponent: MicrosoftOptions, + }, { key: "facebookAuth", title: "Facebook", logo: "facebook.svg", }, { - key: "microsoftAuth", - title: "Microsoft", - logo: "microsoft.svg", - optionsComponent: MicrosoftOptions, + key: "instagramAuth", + title: "Instagram", + logo: "instagram.svg", }, { key: "githubAuth",