From c1b3fba344a1b71225969c2bb20b60ef8279cf25 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Tue, 3 Jan 2023 23:31:53 +0400 Subject: [PATCH] add backend support for Apple auth provider It's a bit different from other OAuth providers and requires a different set of options and a private key file. --- backend/app/cmd/server.go | 44 ++++++++++++++----- backend/app/cmd/server_test.go | 6 ++- backend/app/cmd/testdata/apple.p8 | 6 +++ backend/go.mod | 2 +- backend/go.sum | 2 + .../github.com/go-pkgz/auth/provider/apple.go | 6 ++- backend/vendor/modules.txt | 2 +- .../docs/configuration/authorization/index.md | 21 ++++++++- .../docs/configuration/parameters/index.md | 4 ++ site/src/pages/index.md | 2 +- 10 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 backend/app/cmd/testdata/apple.p8 diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 51d2aae645..0cc964c2b7 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -96,16 +96,17 @@ type ServerCommand struct { SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"` SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint - Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"` - Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"` - Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"` - Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"` - Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"` - Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"` - Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"` - Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"` - Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"` - Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"` + Apple AppleGroup `group:"apple" namespace:"apple" env-namespace:"APPLE" description:"Apple OAuth"` + Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"` + Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"` + Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"` + Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"` + Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"` + Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"` + Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"` + Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"` + Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"` + Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"` Email struct { Enable bool `long:"enable" env:"ENABLE" description:"enable auth via email"` From string `long:"from" env:"FROM" description:"from email address"` @@ -133,6 +134,14 @@ type ImageProxyGroup struct { CacheExternal bool `long:"cache-external" env:"CACHE_EXTERNAL" description:"enable caching for external images"` } +// AppleGroup defines options for Apple auth params +type AppleGroup struct { + CID string `long:"cid" env:"CID" description:"Apple client ID"` + TID string `long:"tid" env:"TID" description:"Apple service ID"` + KID string `long:"kid" env:"KID" description:"Private key ID"` + PrivateKeyFilePath string `long:"private-key-filepath" env:"PRIVATE_KEY_FILEPATH" description:"Private key file location" default:"/var/apple.p8"` +} + // AuthGroup defines options group for auth params type AuthGroup struct { CID string `long:"cid" env:"CID" description:"OAuth client ID"` @@ -829,12 +838,27 @@ func (s *ServerCommand) makeCache() (LoadingCache, error) { return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type) } +//nolint:gocyclo // simple code but many if checks func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error { providersCount := 0 if s.Auth.Telegram { providersCount++ } + if s.Auth.Apple.CID != "" && s.Auth.Apple.TID != "" && s.Auth.Apple.KID != "" { + err := authenticator.AddAppleProvider( + provider.AppleConfig{ + ClientID: s.Auth.Apple.CID, + TeamID: s.Auth.Apple.TID, + KeyID: s.Auth.Apple.KID, + }, + provider.LoadApplePrivateKeyFromFile(s.Auth.Apple.PrivateKeyFilePath), + ) + if err != nil { + return err + } + providersCount++ + } if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" { authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC) providersCount++ diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 8b0c6281d6..0010672ae0 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -79,7 +79,7 @@ func TestServerApp_DevMode(t *testing.T) { waitForHTTPServerStart(port) providers := app.restSrv.Authenticator.Providers() - require.Equal(t, 9+1, len(providers), "extra auth provider") + require.Equal(t, 10+1, len(providers), "extra auth provider") assert.Equal(t, "dev", providers[len(providers)-2].Name(), "dev auth provider") // send ping resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/ping", port)) @@ -107,7 +107,7 @@ func TestServerApp_AnonMode(t *testing.T) { waitForHTTPServerStart(port) providers := app.restSrv.Authenticator.Providers() - require.Equal(t, 9+1, len(providers), "extra auth provider for anon") + require.Equal(t, 10+1, len(providers), "extra auth provider for anon") assert.Equal(t, "anonymous", providers[len(providers)-1].Name(), "anon auth provider") client := http.Client{Timeout: 10 * time.Second} @@ -758,6 +758,8 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation, cmd.Image.FS.Path = "/tmp/remark42_test", "fs", "/tmp/remark42_test", "/tmp/remark42_test" cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port) cmd.Store.Bolt.Timeout = 10 * time.Second + cmd.Auth.Apple.CID, cmd.Auth.Apple.KID, cmd.Auth.Apple.TID = "cid", "kid", "tid" + cmd.Auth.Apple.PrivateKeyFilePath = "testdata/apple.p8" cmd.Auth.Github.CSEC, cmd.Auth.Github.CID = "csec", "cid" cmd.Auth.Google.CSEC, cmd.Auth.Google.CID = "csec", "cid" cmd.Auth.Facebook.CSEC, cmd.Auth.Facebook.CID = "csec", "cid" diff --git a/backend/app/cmd/testdata/apple.p8 b/backend/app/cmd/testdata/apple.p8 new file mode 100644 index 0000000000..aa19d7347e --- /dev/null +++ b/backend/app/cmd/testdata/apple.p8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgGH2MylyZjjRdauTk +xxXW6p8VSHqIeVRRKSJPg1xn6+KgCgYIKoZIzj0DAQehRANCAAS/mNzQ7aBbIBr3 +DiHiJGIDEzi6+q3mmyhH6ZWQWFdFei2qgdyM1V6qtRPVq+yHBNSBebbR4noE/IYO +hMdWYrKn +-----END PRIVATE KEY----- diff --git a/backend/go.mod b/backend/go.mod index 831a11dedc..22194c131d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -11,7 +11,7 @@ require ( github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.2 - github.com/go-pkgz/auth v1.20.1-0.20221226231300-65f433fba0f1 + github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7 github.com/go-pkgz/jrpc v0.3.0 github.com/go-pkgz/lcw v1.0.3-0.20221226231215-a66ea7c4aff7 github.com/go-pkgz/lgr v0.10.4 diff --git a/backend/go.sum b/backend/go.sum index 4a1d6a8616..2ec4ad591c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -142,6 +142,8 @@ github.com/go-oauth2/oauth2/v4 v4.5.1 h1:3vxp+cjLqDe1TbogbwtMyeHRHr1tD+ksrK7xNpp github.com/go-oauth2/oauth2/v4 v4.5.1/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ= github.com/go-pkgz/auth v1.20.1-0.20221226231300-65f433fba0f1 h1:MJA4rZAwjd+KpaR2PqrxeDPloNu9Wml1UVQjL2fOtVM= github.com/go-pkgz/auth v1.20.1-0.20221226231300-65f433fba0f1/go.mod h1:fG1CP4+LDPnebYeO1BAZg/euTQQ8cnGn+5ZrXvJfckA= +github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7 h1:ktKI3Y3UytkBLL1cOEzJmAi3nNKeaRGOzDj51Kgqp6M= +github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7/go.mod h1:fG1CP4+LDPnebYeO1BAZg/euTQQ8cnGn+5ZrXvJfckA= github.com/go-pkgz/email v0.3.1-0.20221002173339-19d25a20d99c/go.mod h1:TpnmSLkQW3FyICit2hn7WIhCUDrhCX6btzz5wS3wHRI= github.com/go-pkgz/email v0.4.1 h1:2vtP2gibsSzqhz6eD5DklSp11m657XEVf17fuXaxMvk= github.com/go-pkgz/email v0.4.1/go.mod h1:BdxglsQnymzhfdbnncEE72a6DrucZHy6I+42LK2jLEc= diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/apple.go b/backend/vendor/github.com/go-pkgz/auth/provider/apple.go index 948aa8fbc7..3ff3c9d0a3 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/apple.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/apple.go @@ -208,7 +208,11 @@ func (ah *AppleHandler) initPrivateKey() error { if err != nil { return err } - ah.conf.publicKey = ah.conf.privateKey.(*ecdsa.PrivateKey).Public() + publicKey, ok := ah.conf.privateKey.(*ecdsa.PrivateKey) + if !ok { + return fmt.Errorf("provided private key is not ECDSA") + } + ah.conf.publicKey = publicKey.Public() ah.conf.clientSecret, err = ah.createClientSecret() if err != nil { return err diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt index 0e757ebd8b..e7e9c2d5b5 100644 --- a/backend/vendor/modules.txt +++ b/backend/vendor/modules.txt @@ -65,7 +65,7 @@ github.com/go-chi/render github.com/go-oauth2/oauth2/v4 github.com/go-oauth2/oauth2/v4/errors github.com/go-oauth2/oauth2/v4/server -# github.com/go-pkgz/auth v1.20.1-0.20221226231300-65f433fba0f1 +# github.com/go-pkgz/auth v1.20.1-0.20230103203948-168bd5a101b7 ## explicit; go 1.17 github.com/go-pkgz/auth github.com/go-pkgz/auth/avatar diff --git a/site/src/docs/configuration/authorization/index.md b/site/src/docs/configuration/authorization/index.md index 4a7a6644fe..dab52a0aa8 100644 --- a/site/src/docs/configuration/authorization/index.md +++ b/site/src/docs/configuration/authorization/index.md @@ -1,4 +1,4 @@ ---- +**--- title: Authorization --- @@ -6,6 +6,23 @@ title: Authorization Authentication handled by external providers. You should set up OAuth2 for at least one of them to allow users to make comments. It is not mandatory to have all of them, but one should be correctly configured. +### Apple (not implemented yet) + +1. Log in [to the developer account](https://developer.apple.com/account). +1. If you don't have an App ID yet, [create one](https://developer.apple.com/account/resources/identifiers/add/bundleId). Later on, you'll need **TeamID**, which is an "App ID Prefix" value. +1. Enable the "Sign in with Apple" capability for your App ID in [the Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) section. +1. Create [Service ID](https://developer.apple.com/account/resources/identifiers/list/serviceId) and bind with App ID from the previous step. Apple will display the description field value to end-users on sign-in. You'll need that service **Identifier as a ClientID** later on. +1. Configure "Sign in with Apple" for created Service ID. Add domain where you will use that auth on to "Domains and subdomains" and its main page URL (like `https://example.com/` to "Return URLs". +1. Register a [New Key](https://developer.apple.com/account/resources/authkeys/list) (**private key**) for the "Sign in with Apple" feature and download it, you'll need to put it to `/var/apple.p8` path inside container. Also write down the private **Key ID**. +1. Add your Remark42 domain name and sender email in the Certificates, Identifiers & Profiles >> [More](https://developer.apple.com/account/resources/services/configure) section as a new Email Source. + +After completing the previous steps, you can proceed with configuring the Apple auth provider. You'll need to set the following environment variables: + +- `AUTH_APPLE_CID` (**required**) - Client ID +- `AUTH_APPLE_TID` (**required**) - Team ID +- `AUTH_APPLE_KID` (**required**) - Private Key ID +- `AUTH_APPLE_PRIVATE_KEY_FILEPATH` (default `/var/apple.p8`) - Private key file location + ### Facebook 1. Open the list of apps on the [Facebook Developers Platform](https://developers.facebook.com/apps) @@ -93,4 +110,4 @@ For more details refer to [Yandex OAuth](https://yandex.com/dev/oauth/doc/dg/con Optionally, anonymous access can be turned on. In this case, an extra `anonymous` provider will allow logins without any social login with any name satisfying two conditions: - the name should be at least three characters long -- the name has to start from the letter and contains letters, numbers, underscores and spaces only +- the name has to start from the letter and contains letters, numbers, underscores and spaces only** diff --git a/site/src/docs/configuration/parameters/index.md b/site/src/docs/configuration/parameters/index.md index a91d2cfc7b..eb2da723e5 100644 --- a/site/src/docs/configuration/parameters/index.md +++ b/site/src/docs/configuration/parameters/index.md @@ -80,6 +80,10 @@ services: | auth.ttl.cookie | AUTH_TTL_COOKIE | `200h` | cookie TTL | | auth.send-jwt-header | AUTH_SEND_JWT_HEADER | `false` | send JWT as a header instead of a cookie | | auth.same-site | AUTH_SAME_SITE | `default` | set same site policy for cookies (`default`, `none`, `lax` or `strict`) | +| auth.apple.cid | AUTH_APPLE_CID | | Apple client ID | +| auth.apple.tid | AUTH_APPLE_TID | | Apple service ID | +| auth.apple.kid | AUTH_APPLE_KID | | Private key ID | +| auth.apple.private-key-filepath | AUTH_APPLE_PRIVATE_KEY_FILEPATH | `/var/apple.p8` | Private key file location | | auth.google.cid | AUTH_GOOGLE_CID | | Google OAuth client ID | | auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret | | auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID | diff --git a/site/src/pages/index.md b/site/src/pages/index.md index ea619aa27f..5c62a147e6 100644 --- a/site/src/pages/index.md +++ b/site/src/pages/index.md @@ -8,7 +8,7 @@ title: Remark42 – Privacy-focused lightweight commenting engine Remark42 allows you to have a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments. -* Social login via Google, Twitter, Facebook, Microsoft, GitHub, Yandex, Patreon and Telegram +* Social login via Google, Twitter, Facebook, Microsoft, GitHub, Apple, Yandex, Patreon and Telegram * Login via email * Optional anonymous access * Multi-level nested comments with both tree and plain presentations