diff --git a/client/options.go b/client/options.go index fdcbe1d2..dbca8c0e 100644 --- a/client/options.go +++ b/client/options.go @@ -87,6 +87,11 @@ func WithBasicAuth(user, pass string) PublishOption { return WithHeader("Authorization", util.BasicAuth(user, pass)) } +// WithBearerAuth adds the Authorization header for Bearer auth to the request +func WithBearerAuth(token string) PublishOption { + return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token)) +} + // WithNoCache instructs the server not to cache the message server-side func WithNoCache() PublishOption { return WithHeader("X-Cache", "no") diff --git a/cmd/publish.go b/cmd/publish.go index 83d79113..be00dfd1 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -35,6 +35,7 @@ var flagsPublish = append( &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"}, + &cli.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token used to auth against the server"}, &cli.IntFlag{Name: "wait-pid", Aliases: []string{"wait_pid", "pid"}, EnvVars: []string{"NTFY_WAIT_PID"}, Usage: "wait until PID exits before publishing"}, &cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"wait_cmd", "cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"}, &cli.BoolFlag{Name: "no-cache", Aliases: []string{"no_cache", "C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"}, @@ -99,10 +100,18 @@ func execPublish(c *cli.Context) error { file := c.String("file") email := c.String("email") user := c.String("user") + token := c.String("token") noCache := c.Bool("no-cache") noFirebase := c.Bool("no-firebase") quiet := c.Bool("quiet") pid := c.Int("wait-pid") + + // Checks + if user != "" && token != "" { + return errors.New("cannot set both --user and --token") + } + + // Do the things topic, message, command, err := parseTopicMessageCommand(c) if err != nil { return err @@ -144,6 +153,9 @@ func execPublish(c *cli.Context) error { if noFirebase { options = append(options, client.WithNoFirebase()) } + if token != "" { + options = append(options, client.WithBearerAuth(token)) + } if user != "" { var pass string parts := strings.SplitN(user, ":", 2) diff --git a/docs/config.md b/docs/config.md index fd5c6154..d8b0ef3a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -222,6 +222,39 @@ User `ben` has three topic-specific entries. He can read, but not write to topic to topic `garagedoor` and all topics starting with the word `alerts` (wildcards). Clients that are not authenticated (called `*`/`everyone`) only have read access to the `announcements` and `server-stats` topics. +### Access tokens +In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful +to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may +want to use a dedicated token to publish from your backup host, and one from your home automation system. + +!!! info + As of today, access tokens grant users **full access to the user account**. Aside from changing the password, + and deleting the account, every action can be performed with a token. Granular access tokens are on the roadmap, + but not yet implemented. + +The `ntfy token` command can be used to manage access tokens for users. Tokens can have labels, and they can expire +automatically (or never expire). Each user can have up to 20 tokens (hardcoded). + +**Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details): +``` +ntfy token list # Shows list of tokens for all users +ntfy token list phil # Shows list of tokens for user phil +ntfy token add phil # Create token for user phil which never expires +ntfy token add --expires=2d phil # Create token for user phil which expires in 2 days +ntfy token remove phil tk_th2sxr... # Delete token +``` + +**Creating an access token:** +``` +$ ntfy token add --expires=30d --label="backups" phil +$ ntfy token list +user phil +- tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 (backups), expires 15 Mar 23 14:33 EDT, accessed from 0.0.0.0 at 13 Feb 23 13:33 EST +``` + +Once an access token is created, you can **use it to authenticate against the ntfy server, e.g. when you publish or +subscribe to topics**. To learn how, check out [authenticate via access tokens](publish.md#access-tokens). + ### Example: Private instance The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`: diff --git a/docs/publish.md b/docs/publish.md index e69be44e..8476ca6f 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -2591,23 +2591,22 @@ title `You've Got Mail` to topic `sometopic` (see [ntfy.sh/sometopic](https://nt
Publishing a message via e-mail
-## Advanced features - -### Authentication +## Authentication Depending on whether the server is configured to support [access control](config.md#access-control), some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can: -* Use [basic auth](#basic-auth), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` -* or use the [`auth` query parameter](#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw` +* Use [username & password](#username-password) via Basic auth, e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk` +* Use [access tokens](#bearer-auth) via Bearer/Basic auth, e.g. `Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2` +* or use either with the [`auth` query parameter](#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw` !!! warning - Base64 only encodes username and password. It **is not encrypting it**. For your self-hosted server, - **be sure to use HTTPS to avoid eavesdropping** and exposing your password. + When using Basic auth, base64 only encodes username and password. It **is not encrypting it**. For your + self-hosted server, **be sure to use HTTPS to avoid eavesdropping** and exposing your password. -#### Basic auth -Here's an example using [Basic auth](https://en.wikipedia.org/wiki/Basic_access_authentication), with a user `testuser` -and password `fakepassword`: +### Username & password +The simplest way to authenticate against a ntfy server is to use [Basic auth](https://en.wikipedia.org/wiki/Basic_access_authentication). +Here's an example with a user `testuser` and password `fakepassword`: === "Command line (curl)" ``` @@ -2701,7 +2700,172 @@ The following command will generate the appropriate value for you on *nix system echo "Basic $(echo -n 'testuser:fakepassword' | base64)" ``` -#### Query param +### Access tokens +In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful +to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may +want to use a dedicated token to publish from your backup host, and one from your home automation system. + +You can create access tokens using the `ntfy token` command, or in the web app in the "Account" section (when logged in). +See [access tokens](config.md#access-tokens) for details. + +Once an access token is created, you can use it to authenticate against the ntfy server, e.g. when you publish or +subscribe to topics. Here's an example using [Bearer auth](https://swagger.io/docs/specification/authentication/bearer-authentication/), +with the token `tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2`: + +=== "Command line (curl)" + ``` + curl \ + -H "Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2" \ + -d "Look ma, with auth" \ + https://ntfy.example.com/mysecrets + ``` + +=== "ntfy CLI" + ``` + ntfy publish \ + --token tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \ + ntfy.example.com/mysecrets \ + "Look ma, with auth" + ``` + +=== "HTTP" + ``` http + POST /mysecrets HTTP/1.1 + Host: ntfy.example.com + Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 + + Look ma, with auth + ``` + +=== "JavaScript" + ``` javascript + fetch('https://ntfy.example.com/mysecrets', { + method: 'POST', // PUT works too + body: 'Look ma, with auth', + headers: { + 'Authorization': 'Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2' + } + }) + ``` + +=== "Go" + ``` go + req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets", + strings.NewReader("Look ma, with auth")) + req.Header.Set("Authorization", "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2") + http.DefaultClient.Do(req) + ``` + +=== "PowerShell" + ``` powershell + $uri = "https://ntfy.example.com/mysecrets" + $headers = @{Authorization="Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"} + $message = "Look ma, with auth" + Invoke-RestMethod -Uri $uri -Body $message -Headers $headers -Method "Post" -UseBasicParsing + ``` + +=== "Python" + ``` python + requests.post("https://ntfy.example.com/mysecrets", + data="Look ma, with auth", + headers={ + "Authorization": "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2" + }) + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', // PUT also works + 'header' => + 'Content-Type: text/plain\r\n' . + 'Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2', + 'content' => 'Look ma, with auth' + ] + ])); + ``` + +Alternatively, you can use [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) to send the +access token. When sending an empty username, the basic auth password is treated by the ntfy server as an +access token. This is primarily useful to make `curl` calls easier, e.g. `curl -u:tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 ...`: + +=== "Command line (curl)" + ``` + curl \ + -u :tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \ + -d "Look ma, with auth" \ + https://ntfy.example.com/mysecrets + ``` + +=== "ntfy CLI" + ``` + ntfy publish \ + --token tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \ + ntfy.example.com/mysecrets \ + "Look ma, with auth" + ``` + +=== "HTTP" + ``` http + POST /mysecrets HTTP/1.1 + Host: ntfy.example.com + Authorization: Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy + + Look ma, with auth + ``` + +=== "JavaScript" + ``` javascript + fetch('https://ntfy.example.com/mysecrets', { + method: 'POST', // PUT works too + body: 'Look ma, with auth', + headers: { + 'Authorization': 'Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy' + } + }) + ``` + +=== "Go" + ``` go + req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets", + strings.NewReader("Look ma, with auth")) + req.Header.Set("Authorization", "Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy") + http.DefaultClient.Do(req) + ``` + +=== "PowerShell" + ``` powershell + $uri = "https://ntfy.example.com/mysecrets" + $headers = @{Authorization="Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy"} + $message = "Look ma, with auth" + Invoke-RestMethod -Uri $uri -Body $message -Headers $headers -Method "Post" -UseBasicParsing + ``` + +=== "Python" + ``` python + requests.post("https://ntfy.example.com/mysecrets", + data="Look ma, with auth", + headers={ + "Authorization": "Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy" + }) + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', // PUT also works + 'header' => + 'Content-Type: text/plain\r\n' . + 'Authorization: Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy', + 'content' => 'Look ma, with auth' + ] + ])); + ``` + + +### Query param Here's an example using the `auth` query parameter: === "Command line (curl)" @@ -2786,6 +2950,8 @@ The following command will generate the appropriate value for you on *nix system echo -n "Basic `echo -n 'testuser:fakepassword' | base64`" | base64 | tr -d '=' ``` +## Advanced features + ### Message caching !!! info If `Cache: no` is used, messages will only be delivered to connected subscribers, and won't be re-delivered if a diff --git a/server/server.go b/server/server.go index 75c61a67..b0d57246 100644 --- a/server/server.go +++ b/server/server.go @@ -38,7 +38,6 @@ import ( - HIGH Docs - tiers - api - - tokens */