diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..385dd2f --- /dev/null +++ b/CONFIGURATION.md @@ -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. diff --git a/cmd/server.go b/cmd/server.go index 2cb016d..a2523b6 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -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") } diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..d9bc48f --- /dev/null +++ b/server/README.md @@ -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. diff --git a/server/auth/README.md b/server/auth/README.md index f79463a..6f42991 100644 --- a/server/auth/README.md +++ b/server/auth/README.md @@ -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":"","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": "", "token": "", "expiryDate": "" } +``` + +### 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": "", + "isAdmin": , + "isUserCreator": , + "expiryDate": +} +. + ``` -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":"","token":"","expiryDate":} +``` + +Returns: + +``` +.. +``` + +### Usage + +When included in the `Authorization` http header, it gives you access based on +your user role. + +``` +Authorization: Bearer .. +``` + +### 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 10min), a new token -is requested with a 'long lived' refresh-token (lifetime of ~1 week) that is -stored in a database. +If a token has expired (either by a server restart or after 10 min), a new token +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.