add doc for auth-flow, server security & conf file

This commit is contained in:
gilex-dev 2023-11-17 15:29:44 +01:00
parent b47536015d
commit 76043829a4
4 changed files with 314 additions and 65 deletions

92
CONFIGURATION.md Normal file
View File

@ -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.

View 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")
} }

22
server/README.md Normal file
View File

@ -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.

View File

@ -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 10min), 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.