Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support additional authentication methods - Oauth #29

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0ecc411
testing
CubicrootXYZ May 4, 2021
de3f94f
remove testing
CubicrootXYZ May 4, 2021
2c4400e
add PoC for oauth2
CubicrootXYZ May 9, 2021
b79b946
add password anmd user check
CubicrootXYZ May 9, 2021
a247db1
started moving oauth to current authentication
CubicrootXYZ May 11, 2021
7c5ce31
integrate oauth in current auth
CubicrootXYZ May 13, 2021
609eb66
move things again and introduce jwt tokens
CubicrootXYZ May 17, 2021
6ee285d
move to authhandler
CubicrootXYZ May 22, 2021
2dce377
get /auth to work properly
CubicrootXYZ May 23, 2021
e78ffda
stack authentication layers & add documentation
CubicrootXYZ May 23, 2021
f60bb83
add refresh config
CubicrootXYZ May 23, 2021
46027e2
clean up & add file support
CubicrootXYZ May 23, 2021
fc6bc82
move token key to config & clean up
CubicrootXYZ May 30, 2021
393d586
removed replaced code
CubicrootXYZ May 30, 2021
69970e4
Merge branch 'pushbits:master' into oauth-testing
CubicrootXYZ May 30, 2021
cee8001
clean up errors
CubicrootXYZ Jun 2, 2021
0504528
wording & consistency
CubicrootXYZ Jun 3, 2021
364e522
panic when no oauth secrets are set
CubicrootXYZ Jun 3, 2021
a06fe24
clean up & consistency
CubicrootXYZ Jun 3, 2021
1e2114b
register auth handler
CubicrootXYZ Jun 3, 2021
43b2908
move to POST
CubicrootXYZ Jun 3, 2021
48b4f71
added longterm tokens
CubicrootXYZ Jun 5, 2021
5b0c124
use body data for auth
CubicrootXYZ Jun 5, 2021
488f426
reflect changes in readme
CubicrootXYZ Jun 5, 2021
c92b783
let oauth use existing db
CubicrootXYZ Jun 19, 2021
1a1ee00
Merge branch 'master' into oauth-testing
CubicrootXYZ Jul 21, 2021
a5e6472
clean up merge conflicts
CubicrootXYZ Jul 21, 2021
b226a5e
add missing package
CubicrootXYZ Jul 21, 2021
d98aa1c
lowercase errors
CubicrootXYZ Jul 21, 2021
bed622e
allow multiple oauth clients
CubicrootXYZ Sep 12, 2021
4fe8d7f
Merge branch 'master' into oauth-testing
CubicrootXYZ Nov 21, 2021
2c62a49
add ginserver again
CubicrootXYZ Nov 21, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,244 @@ Still, if you haven't tried it yet, we'd encourage you to check it out.
- [ ] Two-factor authentication, [issue](https://github.com/pushbits/server/issues/19)
- [ ] Bi-directional key verification, [issue](https://github.com/pushbits/server/issues/20)

## 🚀 Installation

PushBits is meant to be self-hosted.
That means you have to install it on your own server.

Currently, the only supported way of installing PushBits is via [Docker](https://www.docker.com/) or [Podman](https://podman.io/).
The image is hosted [here on Docker Hub](https://hub.docker.com/r/eikendev/pushbits).

| :warning: **You are advised to install PushBits behind a reverse proxy and enable TLS.** Otherwise, your credentials will be transmitted unencrypted. |
|----------------------------------------------------------------------------------------------------------------------------------------------------------|

## ⚙ Configuration

To see what can be configured, have a look at the `config.sample.yml` file inside the root of the repository.

Settings can optionally be provided via environment variables.
The name of the environment variable is composed of a starting `PUSHBITS_`, followed by the keys of the setting, all
joined with `_`.
As an example, the HTTP port can be provided as an environment variable called `PUSHBITS_HTTP_PORT`.

To get started, here is a Docker Compose file you can use.
```yaml
version: '2'

services:
server:
image: eikendev/pushbits:latest
ports:
- 8080:8080
environment:
PUSHBITS_DATABASE_DIALECT: 'sqlite3'
PUSHBITS_ADMIN_MATRIXID: '@your/matrix/username:matrix.org' # The Matrix account on which the admin will receive their notifications.
PUSHBITS_ADMIN_PASSWORD: 'your/pushbits/password' # The login password of the admin account. Default username is 'admin'.
PUSHBITS_MATRIX_USERNAME: 'your/matrix/username' # The Matrix account from which notifications are sent to all users.
PUSHBITS_MATRIX_PASSWORD: 'your/matrix/password' # The password of the above account.
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- ./data:/data
```

In this example, the configuration file would be located at `./data/config.yml` on the host.
The SQLite database would be written to `./data/pushbits.db`.
**Don't forget to adjust the permissions** of the `./data` directory, otherwise PushBits will fail to operate.

## 📄 Usage

Now, how can you interact with the server?
We provide [a little CLI tool called pbcli](https://github.com/PushBits/cli) to make basic API requests to the server.
It helps you to create new users and applications.
You will find further instructions in the linked repository.

At the time of writing, there is no fancy GUI built-in, and we're not sure if this is necessary at all.
Currently, we would like to avoid front end development, so if you want to contribute in this regard we're happy if you reach out!

After you have created a user and an application, you can use the API to send a push notification to your Matrix account.

```bash
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{"message":"my message","title":"my title"}' \
"https://pushbits.example.com/message?token=$PB_TOKEN"
```

Note that the token is associated with your application and has to be kept secret.
You can retrieve the token using [pbcli](https://github.com/PushBits/cli) by running following command.

```bash
pbcli application show $PB_APPLICATION --url https://pushbits.example.com --username $PB_USERNAME
```

### Authentication

Pushbits offers you two methods of authenticating against the server:

* Basic authentication (`basic`)
* [Oauth 2.0](https://oauth.net/2/) (`oauth`)

You will find the corresponding setting in the security section.

```yaml
...
security:
...
# The authentication method to use
authentication: basic
...
```

#### Basic authentication

For [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) you have to provide your username and password in each request to the server. For example in curl you can do this with the `--user` flag:

```bash
curl -u myusername:totallysecretpassword
```

#### Oauth 2.0

[Oauth 2.0](https://en.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the PushBits server.

Make sure to setup the "oauth" section in the config file correctly.

##### Authenticating

For authentication use the ``/oauth2/auth` endpoint. E.g.:

```bash
curl \
--header "Content-Type: application/json" \
--request POST \
"https://pushbits.example.com/oauth2/auth" -d "client_id=000000&username=admin&password=1233456&response_type=code&redirect_uri=https://myapp.example.com"
```

This will return a HTTP redirect with the status code `302` and an authentication code set as parameter:

```
HTTP/2 302
date: Sun, 23 May 2021 10:33:27 GMT
location: https://myapp.example.com?code=4T1TJXMBPTOS4NNGILBDYW
content-length: 0
```

Your app then needs to use this code to trade it for a access token.

**Hint for command line users:** you can extract the authentication code from the redirect without the need of a running webserver.

##### Receiving an access token

You can get an access token from the `/oauth/token` endpoint. There are several methods, so called "grant types" for receiving a token. PushBits currently supports the following one's:

* Refresh
* Authentication code

Oauth 2.0 authentication is based on "clients", thus you need to provide identifiers for a client with your request. These are the `client_id` and the `client_secret`.

For your first token you will need a authentication code, see the section above. Then use it like this:

```bash
curl \
--header "Content-Type: application/json" \
--request POST \
"https://pushbits.example.com/oauth2/token" -d "grant_type=authorization_code&client_id=000000&client_secret=49gjg4js9&response_type=token&redirect_uri=https://myapp.example.com&code=OP1Q2UJEVL-RPR9GZAUURA"
```

This will then return an access token and refresh token for you.

```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ",
"expires_in": 86400,
"refresh_token": "OP1Q2UJEVL-RPR9GZAUURA",
"token_type": "Bearer"
}
```

The access token is short lived, the refresh token is long lived, but can not be used for authentication. If your access token runs out, you can use the refresh token to generate a new access token:

```bash
curl \
--header "Content-Type: application/json" \
--request POST \
"https://pushbits.example.com/oauth2/token" -d "grant_type=refresh_token&client_id=000000&client_secret=49gjg4js9&response_type=token&refresh_token=OP1Q2UJEVL-RPR9GZAUURA"
```

##### Getting information about a access token

With a valid access token you can get information about it from `/oauth/tokeninfo`. This is meant for testing if a token is issued correctly.

```bash
curl \
--header "Content-Type: application/json" \
--request GET \
--header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \
"https://pushbits.example.com/oauth2/tokeninfo"
```

##### Revoking a token

Admin users are eligible to revoke tokens. This should not be necessary in normal operation, as tokens are only short lived. But there might be situations where attackers might have gotten knowledge about a token.

```bash
curl \
--header "Content-Type: application/json" \
--request POST \
--header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \
--data '{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NDg1MDYsInN1YiI6IjEifQ.cO0_8fqsJDG4KswjC0CSzc_EznntH-FDQejdolPAISo"}' \
"https://pushbits.example.com/oauth2/revoke"
```

##### Requesting a longterm token

Longterm tokens are tokens that life for multiple years. They can be used for scripts and other software that access PushBits. So the other software does not need knowledge about the actuall password of the user. However be carefull with longterm tokens, if you loose one others might be able to perform actions on your user account.

```bash
curl \
--request POST \
--header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \
--data '{"client_id": "000000", "client_secret": "49gjg4js9"}' \
"https://push.remote.alexanderebhart.de/oauth2/longtermtoken"
```

### Message options

Messages can be specified in three different syntaxes:

* `text/plain`
* `text/html`
* `text/markdown`

To set a specific syntax you need to set the `extras` parameter ([inspired by Gotify's message extras](https://gotify.net/docs/msgextras#clientdisplay)):

```bash
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{"message":"my message with\n\n**Markdown** _support_.","title":"my title","extras":{"client::display":{"contentType": "text/markdown"}}}' \
"https://pushbits.example.com/message?token=$PB_TOKEN"
```

HTML content might not be fully rendered in your Matrix client; see the corresponding [Matrix specs](https://spec.matrix.org/unstable/client-server-api/#mroommessage-msgtypes).
This also holds for Markdown, as it is translated into the corresponding HTML syntax.

### Deleting a Message

You can delete a message, this will send a notification in response to the original message informing you that the message is "deleted".

To delete a message, you need its message ID which is provided as part of the response when you send the message.
The ID might contain characters not valid in URIs.
We hence provide an additional `id_url_encoded` field for messages; you can directly use it when deleting a message without performing encoding yourself.

```bash
curl \
--request DELETE \
"https://pushbits.example.com/message/${MESSAGE_ID}?token=$PB_TOKEN"
```

## 👮 Acknowledgments

The idea for this software and most parts of the initial source are heavily inspired by [Gotify](https://gotify.net/).
Expand Down
3 changes: 2 additions & 1 deletion cmd/pushbits/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func main() {

cm := credentials.CreateManager(c.Security.CheckHIBP, c.Crypto)

log.Println(c.Database.Dialect)
db, err := database.Create(cm, c.Database.Dialect, c.Database.Connection)
if err != nil {
log.Fatal(err)
Expand All @@ -75,7 +76,7 @@ func main() {
log.Fatal(err)
}

engine := router.Create(c.Debug, cm, db, dp)
engine := router.Create(c.Debug, cm, db, dp, c)

runner.Run(engine, c.HTTP.ListenAddress, c.HTTP.Port)
}
15 changes: 15 additions & 0 deletions config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,18 @@ crypto:
formatting:
# Whether to use colored titles based on the message priority (<0: grey, 0-3: default, 4-10: yellow, 10-20: orange, >20: red).
coloredtitle: false

authentication:
# The authentication method to use.
method: basic
# Only needed if you choose oauth method.
oauth:
clients:
# Oauth client identifier (random string).
- clientid: "000000"
# Oauth client secret (random string).
clientsecret: ""
# Oauth redirect url after successful auth. Redirects will only be accepted for this url.
clientredirect: "http://localhost"
# Key used for signing (JWT with HS512) the access tokens (random string).
tokenkey: ""
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ go 1.16

require (
github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/gin-contrib/location v0.0.2
github.com/gin-gonic/gin v1.7.0
github.com/go-oauth2/gin-server v1.0.0
github.com/golang/protobuf v1.4.3 // indirect
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd
github.com/google/go-cmp v0.5.0 // indirect
github.com/imrenagi/go-oauth2-mysql v1.1.0
github.com/jinzhu/configor v1.2.1
github.com/jmoiron/sqlx v1.3.3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is related to the separate sqlite file: What's the reason for switching to sqlx when we already have GORM?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see now that it is integrated with oauth. Maybe we can investigate this further. Having two different databases increases complexity.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not very clean, but it is possible to use the same mysql database for auth and pushbits itself by just using the same credentials.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I managed to improve that one step further - oauth now does not need any further storage information. If the main db is mysql, oauth uses just the same database and if it is sqlite3 it uses its own sqlite file.

Unfortunately I was not yet able to share the main sqlite with oauth. For that I'd need a sqlx object with the sqlite - which I am able to generate but it then runs in "command unknown" errors as soon as actions are performed on it.

github.com/json-iterator/go v1.1.10 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd
Expand All @@ -20,6 +24,7 @@ require (
github.com/ugorji/go v1.2.4 // indirect
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/oauth2.v3 v3.12.0 // indirect
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.0.4
gorm.io/driver/sqlite v1.1.4
Expand Down
Loading