add doc for auth-flow, server security & conf file
This commit is contained in:
parent
b47536015d
commit
76043829a4
|
@ -0,0 +1,92 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
All configurations are stored in `.YetAnotherToDoList.yaml`, either in the
|
||||||
|
current working directory or the users home directory.
|
||||||
|
|
||||||
|
You can also use a custom name & path by setting the `--config` flag followed by
|
||||||
|
the path to your file.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
sqlite3File: 'YetAnotherToDoList.sqlite3'
|
||||||
|
secret: 'anAlphaNumericSecretKey123'
|
||||||
|
initialAdmin:
|
||||||
|
userName: 'anAdminUser'
|
||||||
|
password: 'anAlphaNumericPassword123'
|
||||||
|
|
||||||
|
logging:
|
||||||
|
logFile: 'YetAnotherToDoList.log'
|
||||||
|
logUTC: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
portHTTP: 4242
|
||||||
|
portHTTPS: 4241
|
||||||
|
certFile: 'certFile.crt'
|
||||||
|
keyFile: 'keyFile.key'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### database
|
||||||
|
|
||||||
|
#### sqlite3File
|
||||||
|
|
||||||
|
The path to the sqlite3 database. This can also be set using the `--sqlite3File`
|
||||||
|
flag.
|
||||||
|
|
||||||
|
#### secret
|
||||||
|
|
||||||
|
A secret string used as password pepper and for signing JWT. All passwords & JWT
|
||||||
|
will become invalid if you change/loose this.
|
||||||
|
|
||||||
|
#### initialAdmin
|
||||||
|
|
||||||
|
Only required for first start/database creation.
|
||||||
|
|
||||||
|
##### userName
|
||||||
|
|
||||||
|
The username for the initial admin user.
|
||||||
|
|
||||||
|
See [username](./server/auth/README.md#username) for requirement.
|
||||||
|
|
||||||
|
##### password
|
||||||
|
|
||||||
|
The password for the initial admin user.
|
||||||
|
|
||||||
|
See [password](./server/auth/README.md#password) for requirement.
|
||||||
|
|
||||||
|
### logging
|
||||||
|
|
||||||
|
#### logFile
|
||||||
|
|
||||||
|
The path to the log file. Defaults to stdout.
|
||||||
|
|
||||||
|
#### logUTC
|
||||||
|
|
||||||
|
A bool whether to use UTC or local time in the logs.
|
||||||
|
|
||||||
|
### server
|
||||||
|
|
||||||
|
#### portHTTP
|
||||||
|
|
||||||
|
The port to listen on for HTTP traffic. Must be between 0 and 65535. Defaults to
|
||||||
|
`4242`.
|
||||||
|
|
||||||
|
This can also be set using the `--portHTTP` or `-p` flag on the `server`
|
||||||
|
subcommand.
|
||||||
|
|
||||||
|
#### portHTTPS
|
||||||
|
|
||||||
|
The port to listen on for HTTPS traffic. Must be between 0 and 65535. Defaults
|
||||||
|
to `4241`. This can also be set using the `--portHTTPS` flag on the `server`
|
||||||
|
subcommand.
|
||||||
|
|
||||||
|
#### certFile
|
||||||
|
|
||||||
|
The path to the certificate file.
|
||||||
|
|
||||||
|
#### keyFile
|
||||||
|
|
||||||
|
The path to the key file matching the certificate file.
|
|
@ -33,7 +33,10 @@ var serverCmd = &cobra.Command{
|
||||||
if err := viper.BindPFlag("debug", cmd.Flags().Lookup("debug")); err != nil {
|
if err := viper.BindPFlag("debug", cmd.Flags().Lookup("debug")); err != nil {
|
||||||
globals.Logger.Println("Unable to bind flag:", err)
|
globals.Logger.Println("Unable to bind flag:", err)
|
||||||
}
|
}
|
||||||
if err := viper.BindPFlag("port", cmd.Flags().Lookup("port")); err != nil {
|
if err := viper.BindPFlag("server.portHTTP", cmd.Flags().Lookup("portHTTP")); err != nil {
|
||||||
|
globals.Logger.Println("Unable to bind flag:", err)
|
||||||
|
}
|
||||||
|
if err := viper.BindPFlag("server.portHTTPS", cmd.Flags().Lookup("portHTTPS")); err != nil {
|
||||||
globals.Logger.Println("Unable to bind flag:", err)
|
globals.Logger.Println("Unable to bind flag:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +57,7 @@ func init() {
|
||||||
// Cobra supports local flags which will only run when this command
|
// Cobra supports local flags which will only run when this command
|
||||||
// is called directly, e.g.:
|
// is called directly, e.g.:
|
||||||
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
serverCmd.Flags().BoolP("debug", "d", false, "Enable debugging")
|
serverCmd.Flags().BoolP("debug", "d", false, "Enable debugging (unused)")
|
||||||
serverCmd.Flags().IntP("port", "p", 4242, "The port to listen on")
|
serverCmd.Flags().IntP("portHTTP", "p", 4242, "The port to listen on for HTTP")
|
||||||
|
serverCmd.Flags().Int("portHTTPS", 4241, "The port to listen on for HTTPS")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Server
|
||||||
|
|
||||||
|
## SSL/TLS
|
||||||
|
|
||||||
|
You can generate a self-signed certificate for testing like this:
|
||||||
|
|
||||||
|
```bass
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout keyFile.key -out certFile.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
Or obtain a signed certificate from [let's encrypt](https://letsencrypt.org/).
|
||||||
|
|
||||||
|
## CSRF
|
||||||
|
|
||||||
|
CSRF should not be possible because we check for the `Authorization` http header
|
||||||
|
(instead of cookies) when accessing protected recourses.
|
||||||
|
|
||||||
|
Because of this, CRIME/BREACH http attacks should also be not possible.
|
||||||
|
|
||||||
|
## XSS
|
||||||
|
|
||||||
|
We rely on Vue.js's ability to escape user-input in templates.
|
|
@ -1,60 +1,196 @@
|
||||||
# Authentication in Golang
|
# Authentication
|
||||||
|
|
||||||
## Use header
|
Authentication in this project works in three steps:
|
||||||
|
|
||||||
Use the http header or the body, but avoid using cookies to transport tokens etc
|
1. first you [login](#login) and retrieve a [refresh token](#refresh-token)
|
||||||
(because of CSRF)
|
2. send your [refresh token](#refresh-token) and retrieve a short-lived
|
||||||
|
[access token](#access-token)
|
||||||
|
3. send your [access token](#access-token) with every api request to access
|
||||||
|
protected recourses
|
||||||
|
|
||||||
## Implementation
|
See the [example](#example) for the most basic authentication flow. Note that
|
||||||
|
the script requests a new refresh-token every time, which should be avoided.
|
||||||
|
|
||||||
```
|
## Login
|
||||||
# create password/user
|
|
||||||
update_password(password):
|
|
||||||
generate salt
|
|
||||||
db.store(hash(salt + password))
|
|
||||||
|
|
||||||
#login
|
Make a request to `/auth/login` with your user credentials (username/user Id &
|
||||||
get_token(userId, password):
|
password) as json inside the `Authorization` http header field:
|
||||||
if not hash(salt + password) == db.get(salted_hash_of_password): # use timing-attack resistant compare here
|
|
||||||
return FAILED
|
|
||||||
generate selector
|
|
||||||
db.store(selector)
|
|
||||||
generate salt
|
|
||||||
generate auth_token
|
|
||||||
db.store(salt, hash(salt+auth_token))
|
|
||||||
return selector:auth_token
|
|
||||||
|
|
||||||
#authenticate
|
```bash
|
||||||
validate_token(selector, auth_token):
|
Authorization: {"userId":"<userId>","password":"<password>"}
|
||||||
if not hash(salt+auth_token) == db.get(salt, salted_hash_of_auth_token WHERE selector): # use timing-attack resistant compare here
|
|
||||||
return UNKNOWN_TOKEN
|
|
||||||
return AUTHENTICATED
|
|
||||||
```
|
```
|
||||||
|
|
||||||
idea: replace selector with userId?
|
The order of the `json` fields does not matter. Note the use of `"` for valid
|
||||||
|
`json`.
|
||||||
|
|
||||||
## JWT
|
### Usage
|
||||||
|
|
||||||
|
If your credentials are valid, you will get a [refresh token](#refresh-token) as
|
||||||
|
a response. Store it somewhere save and use it to request new
|
||||||
|
[access tokens](#access-token).
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
Password and either Username or userId are required.
|
||||||
|
|
||||||
|
#### Password
|
||||||
|
|
||||||
|
Your password must only contain letters and numbers.
|
||||||
|
|
||||||
|
#### Username
|
||||||
|
|
||||||
|
Your username must only contain letters and numbers.
|
||||||
|
|
||||||
|
#### User Id
|
||||||
|
|
||||||
|
Your userId.
|
||||||
|
|
||||||
|
## Refresh Token
|
||||||
|
|
||||||
|
A json object containing a selector, token and expiry date used to request
|
||||||
|
[access tokens](#access-token)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "selector": "<string>", "token": "<string>", "expiryDate": "<int>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Obtaining
|
||||||
|
|
||||||
|
See [login](#login).
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Used to [request an access token](#obtaining-1).
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
All fields are required.
|
||||||
|
|
||||||
|
#### selector
|
||||||
|
|
||||||
|
The base64 encoded URL-save representation of a random `byte[9]` array used to
|
||||||
|
retrieved the associated token-hash from the database.
|
||||||
|
|
||||||
|
#### token
|
||||||
|
|
||||||
|
The base64 encoded URL-save representation of a `byte[33]` array used to compare
|
||||||
|
against a hash stored in the database.
|
||||||
|
|
||||||
|
#### expiryDate
|
||||||
|
|
||||||
|
An integer representing the UNIX timestamp at which the associated refresh token
|
||||||
|
becomes invalid.
|
||||||
|
|
||||||
|
Currently hardcoded to **10 days** inside
|
||||||
|
[/database/crypto_helpers.go](../../database/crypto_helpers.go):
|
||||||
|
`const refreshTokenLifetime = "+10 day"`.
|
||||||
|
|
||||||
|
## Access Token
|
||||||
|
|
||||||
|
A JWT (JSON Web Token, [learn more](https://jwt.io/)) consisting of three base64
|
||||||
|
encoded URL-save parts separated by dots `.`.
|
||||||
|
|
||||||
|
Decoded & formatted:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"userId": "id",
|
"alg": "HS256",
|
||||||
"userRole": "role",
|
"typ": "JWT"
|
||||||
"expiryTime": "now+10min"
|
|
||||||
}
|
}
|
||||||
|
.
|
||||||
|
{
|
||||||
|
"userId": "<int>",
|
||||||
|
"isAdmin": <bool>,
|
||||||
|
"isUserCreator": <bool>,
|
||||||
|
"expiryDate": <int>
|
||||||
|
}
|
||||||
|
.
|
||||||
|
<signature>
|
||||||
```
|
```
|
||||||
|
|
||||||
We use JWT with a lifespan of 10min and an in-memory db to blacklist revoked
|
### Obtaining
|
||||||
tokens. So if for e.g. a user changes it's password, we would add the userId and
|
|
||||||
the time of the change to the blacklist, filtering out all tokens that have been
|
Make a request to `/auth` with a [refresh token](#refresh-token) inside the
|
||||||
issued before.
|
`Authorization` http header:
|
||||||
|
|
||||||
|
```json
|
||||||
|
Authorization: Refresh {"selector":"<string>","token":"<string>","expiryDate":<int>}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
```
|
||||||
|
<base64 encoded header>.<base64 encoded payload>.<base64 encoded signature>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
When included in the `Authorization` http header, it gives you access based on
|
||||||
|
your user role.
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <base64 encoded header>.<base64 encoded payload>.<base64 encoded signature>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
All fields are required.
|
||||||
|
|
||||||
|
#### Header
|
||||||
|
|
||||||
|
##### alg
|
||||||
|
|
||||||
|
The algorithm used to create the signature. Currently only `HS256` is supported.
|
||||||
|
|
||||||
|
##### type
|
||||||
|
|
||||||
|
The **IANA Media Type**
|
||||||
|
([learn more](https://www.iana.org/assignments/media-types/media-types.xhtml))
|
||||||
|
of the payload. This field is always set to `"jwt"`.
|
||||||
|
|
||||||
|
#### Payload
|
||||||
|
|
||||||
|
##### userId
|
||||||
|
|
||||||
|
The userId associated with the access token.
|
||||||
|
|
||||||
|
##### isAdmin
|
||||||
|
|
||||||
|
A boolean indicating if the user has admin rights.
|
||||||
|
|
||||||
|
##### isUserCreator
|
||||||
|
|
||||||
|
A boolean indicating if the user has user creation rights.
|
||||||
|
|
||||||
|
##### expiryDate
|
||||||
|
|
||||||
|
An integer representing the UNIX timestamp at which the associated refresh token
|
||||||
|
becomes invalid.
|
||||||
|
|
||||||
|
Currently hardcoded to **10 minutes** inside
|
||||||
|
[database/crypto_helpers.go](../../database/crypto_helpers.go):
|
||||||
|
`const accessTokenLifetime = time.Minute * 10`.
|
||||||
|
|
||||||
|
#### Signature
|
||||||
|
|
||||||
|
A `SHA256` hash of the encoded header and payload. The value of
|
||||||
|
`database.secret` from the [config file](../../.YetAnotherToDoList.yaml) is used
|
||||||
|
as a key (salt).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
We use access tokens with a lifespan of 10 min and an in-memory db to blacklist
|
||||||
|
revoked tokens. So if for e.g. a user changes it's password, we would add the
|
||||||
|
userId and the time of the change to the blacklist, invalidating all tokens of
|
||||||
|
that user that have been issued before.
|
||||||
|
|
||||||
After a server restart, all tokens will become invalid as well, since we can not
|
After a server restart, all tokens will become invalid as well, since we can not
|
||||||
be sure which ones were 'on the blacklist'. This could be mitigated by making
|
be sure which ones were revoked (this could be mitigated in the future by making
|
||||||
the blacklist persistent during restarts.
|
the blacklist persist during restarts).
|
||||||
|
|
||||||
If a token has expired (either by a server restart or after 10 min), a new token
|
If a token has expired (either by a server restart or after 10 min), a new token
|
||||||
is requested with a 'long lived' refresh-token (lifetime of ~1 week) that is
|
is requested with a [refresh token](#refresh-token) that is stored in a
|
||||||
stored in a database.
|
database.
|
||||||
|
|
||||||
### Pros:
|
### Pros:
|
||||||
|
|
||||||
|
@ -62,30 +198,25 @@ stored in a database.
|
||||||
|
|
||||||
### Cons:
|
### Cons:
|
||||||
|
|
||||||
- timestamps of blacklisting could become a problem (maybe add 1 second or use
|
- increased load on database after server restart since all active clients need
|
||||||
timestamp returned by n-th node when it has received the update).
|
a new access token.
|
||||||
- increased load on db + service since we need to issue new jwt for everybody.
|
|
||||||
|
|
||||||
## SSL/TLS
|
## Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout keyFile.key -out certFile.crt
|
#!/bin/env bash
|
||||||
|
USERNAME="admin"
|
||||||
|
PASSWORD="temporaryPassword"
|
||||||
|
|
||||||
|
# login (you can replace `userName` with `userId` since the username might change)
|
||||||
|
REFRESH_TOKEN=$(curl -ksH "Authorization: Login {\"userName\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" https://localhost:4241/auth/login)
|
||||||
|
|
||||||
|
# request access token (every 10 minutes or after server restart)
|
||||||
|
ACCESS_TOKEN=$(curl -ksH "Authorization: Refresh $REFRESH_TOKEN" https://localhost:4241/auth)
|
||||||
|
|
||||||
|
# a request to a fictional protected resource
|
||||||
|
QUERY_RESULT=$(curl -ksH "Authorization: Bearer $ACCESS_TOKEN" https://localhost:4241/protected)
|
||||||
|
|
||||||
|
# "convert" the header & access token to a json string to use in client applications (e.g. GraphiQL)
|
||||||
|
echo -e "Use this as header for e.g. GraphiQL:\n{\"Authorization\":\"Bearer $ACCESS_TOKEN\"}"
|
||||||
```
|
```
|
||||||
|
|
||||||
## CRIME/BREACH http compression attack
|
|
||||||
|
|
||||||
While we do not use a CSRF token, headers sent to the `/api` still contain
|
|
||||||
private data.
|
|
||||||
|
|
||||||
If you are using http/1.1 or lower and have compression enabled on your proxy,
|
|
||||||
you would be .
|
|
||||||
|
|
||||||
## CRSF
|
|
||||||
|
|
||||||
We check for a http header like this `X-YOURSITE-CSRF-PROTECTION=1`. This should
|
|
||||||
be enough, according to
|
|
||||||
[cheatsheetseries.owasp.org](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#custom-request-headers)
|
|
||||||
|
|
||||||
## XSS
|
|
||||||
|
|
||||||
We rely on Vue.js's ability to escape user-input in templates.
|
|
||||||
|
|
Loading…
Reference in New Issue