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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -54,6 +57,7 @@ func init() {
|
|||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
serverCmd.Flags().BoolP("debug", "d", false, "Enable debugging")
|
||||
serverCmd.Flags().IntP("port", "p", 4242, "The port to listen on")
|
||||
serverCmd.Flags().BoolP("debug", "d", false, "Enable debugging (unused)")
|
||||
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
|
||||
(because of CSRF)
|
||||
1. first you [login](#login) and retrieve a [refresh token](#refresh-token)
|
||||
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.
|
||||
|
||||
```
|
||||
# create password/user
|
||||
update_password(password):
|
||||
generate salt
|
||||
db.store(hash(salt + password))
|
||||
## Login
|
||||
|
||||
#login
|
||||
get_token(userId, password):
|
||||
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
|
||||
Make a request to `/auth/login` with your user credentials (username/user Id &
|
||||
password) as json inside the `Authorization` http header field:
|
||||
|
||||
#authenticate
|
||||
validate_token(selector, auth_token):
|
||||
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
|
||||
```bash
|
||||
Authorization: {"userId":"<userId>","password":"<password>"}
|
||||
```
|
||||
|
||||
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
|
||||
{
|
||||
"userId": "id",
|
||||
"userRole": "role",
|
||||
"expiryTime": "now+10min"
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
.
|
||||
{
|
||||
"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
|
||||
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
|
||||
issued before.
|
||||
### Obtaining
|
||||
|
||||
Make a request to `/auth` with a [refresh token](#refresh-token) inside the
|
||||
`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
|
||||
be sure which ones were 'on the blacklist'. This could be mitigated by making
|
||||
the blacklist persistent during restarts.
|
||||
be sure which ones were revoked (this could be mitigated in the future by making
|
||||
the blacklist persist during restarts).
|
||||
|
||||
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
|
||||
stored in a database.
|
||||
is requested with a [refresh token](#refresh-token) that is stored in a
|
||||
database.
|
||||
|
||||
### Pros:
|
||||
|
||||
|
@ -62,30 +198,25 @@ stored in a database.
|
|||
|
||||
### Cons:
|
||||
|
||||
- timestamps of blacklisting could become a problem (maybe add 1 second or use
|
||||
timestamp returned by n-th node when it has received the update).
|
||||
- increased load on db + service since we need to issue new jwt for everybody.
|
||||
- increased load on database after server restart since all active clients need
|
||||
a new access token.
|
||||
|
||||
## SSL/TLS
|
||||
## Example
|
||||
|
||||
```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