Initial commit

This commit is contained in:
Yaser
2026-08-13 19:56:46 +03:30
commit de1d57a67b
474 changed files with 43185 additions and 0 deletions

70
docs/general/2fa.md Normal file
View File

@@ -0,0 +1,70 @@
# Setting up 2FA
This document describes Barong [TOTP](https://tools.ietf.org/html/rfc6238) setup
using [Vault](https://www.vaultproject.io/intro/getting-started/install.html).
## Prerequisites
[Vault](https://www.vaultproject.io/intro/getting-started/install.html)
with [TOTP secrets engine](https://www.vaultproject.io/docs/secrets/totp/index.html#setup) enabled.
## Configuration
To use Vault with Barong you will need to set the following environment variables:
```shell
export VAULT_ADDR=http://your-vault-url.com
export VAULT_TOKEN=12345-vault-t0k3n-54321
```
To allow using Google Authenticator `VAULT_ADDR` should be _public_ ip.
Note, that TOTP uses time-based algorithm.
So, if you want to test 2FA with phone, make sure, that your Vault's server time and your phone's time are synchronized, or it will not work.
[ntpdate](http://doc.ntp.org/4.1.1/ntpdate.htm) can help you to update your time with ntp servers:
```shell
sudo ntpdate 0.ua.pool.ntp.org
```
## Developer How-tos
### Getting a code without Google Authenticator:
* From _shell_:
```shell
$ vault login
$ vault read totp/code/IDMYAWESOMEID
```
* From _rails console_:
```ruby
> me = Account.find_by_email('me@example.com')
> Vault.logical.read("totp/code/#{me.uid}")
```
### Getting a new key (e.g. if you lost your Google Authenticator):
* From _shell_:
```shell
$ vault login
$ vault write totp/keys/IDMYAWESOMEID \
generate=true \
issuer=Barong \
account_name=me@example.com
```
* From _rails console_:
```ruby
> me = Account.find_by_email('me@example.com')
> Vault::TOTP.send(:create, me.uid)
```
Each response includes equivalent base64-encoded barcode and OTP url.
You can find the key's secret in this OTP url query params.

View File

@@ -0,0 +1,35 @@
### Activities
To track admin activities you need to define it on seed.yml on `permissions` key
- `role` should be in a range of existing: `admin`, `superadmin`, `support`, `techical`, `accountant`
- `verb` should be `post`, `get`, `put`, `delete`
- `path` - endpoint which should be checked, should be started with `api/v2/#{component}` as prefix
- `action` should be `audit`
```
For example
permissions:
- { role: 'admin', verb: 'post', path: api/v2/admin, action: audit }
```
Here you can see a list of possible fields for activity:
| Field | Type | Description |
|:-----------|:--------:|:-----------:|
| user_id | bigint | ID of user who creates activity |
| target_uid | string | User UID for whom activity was created (admin remove OTP for user, target_uid will be uid of user for which admin removed OTP|
| category | string | `admin` (admin activities), `user` (user activities)|
| user_ip | string | IP address |
| user_agent | string | User Agent such as `Mozilla/5.0`|
| topic | string | Defined topic (`session`, `adjustments`) or `general` by default|
| action | string | API action: `POST => 'create'`, `PUT => 'update'`, `GET => 'read'`, `DELETE => 'delete'`, `PATCH => 'update'` or `system` if there is no match of HTTP method|
| result | string | Status of API response: `succeed`, `failed`, `denied`|
| data | text | Parameters which was sent to specific API endpoint|
| created_at | datetime | Time of activity creation|
##### Useful commands
If you want to delete old activities you can run next command
Be sure that your parameters has valid date string, such as `YYYY-mm-dd` !
```
bundle exec rake activities:delete[from,to]
```

142
docs/general/api-keys.md Normal file
View File

@@ -0,0 +1,142 @@
# Barong API keys creation and usage
This document explains how to create an API key on barong using the UI or command line tool.
This API key can be used to access microservices in the cluster protected by barong authentication.
You can find below an example how to use the API key.
## How to create API key ?
### Using UI (recommended option)
1. Find API keys section (often located on profile page).
![API-keys-section](../images/api-keys-1.jpeg)
2. Create your API key and securely save Access Key and Secret Key
![API-key-creation](../images/api-keys-2.jpeg)
### Using API (use this option in case your frontend doesn't support API keys feature)
1. Install [httpie](https://httpie.org/)
2. Login into your account using httpie
```bash
http --session barong_session https://your.domain/api/v2/barong/identity/sessions \
email=your@email.com password=changeme otp_code=000000
```
Example of response:
```json
{
"created_at": "2020-06-01T07:01:20Z",
"csrf_token": "f5b36515a428328e199a",
"data": "{\"language\":\"en\"}",
"data_storages": [],
"email": "your@example.com",
"labels": [
{
"created_at": "2020-06-01T07:01:45Z",
"key": "email",
"scope": "private",
"updated_at": "2020-06-01T07:01:45Z",
"value": "verified"
}
],
"level": 5,
"otp": true,
"phones": [
{
"country": "FR",
"number": "33*****0471",
"validated_at": "2020-06-01T07:03:18.000Z"
}
],
"profiles": [],
"referral_uid": null,
"role": "member",
"state": "active",
"uid": "IDAF1AED1A42",
"updated_at": "2020-10-22T18:01:09Z"
}
```
3. Validate your session
```bash
http --session barong_session https://your.domain.com/api/v2/peatio/account/balances
```
4. Create your API key
```
http --session barong_session https://your.domain.com/api/v2/barong/resource/api_keys \
algorithm=HS256 totp_code=681757 x-csrf-token:f5b36515a428328e199a
```
Expected response:
```json
{
"algorithm": "HS256",
"created_at": "2019-12-23T12:22:15Z",
"kid": "61d025b8573501c2", // Access Key
"scope": [],
"secret": {
"auth": null,
"data": {
"value": "2d0b4979c7fe6986daa8e21d1dc0644f" // Secret Key
},
"lease_duration": 2764800,
"lease_id": "",
"metadata": null,
"renewable": false,
"warnings": null,
"wrap_info": null
},
"state": "active",
"updated_at": "2019-12-23T12:22:15Z"
}
```
5. Securely save Access Key and Secret Key
## How to use API key ?
To authenticate using API key you need to pass next 3 headers:
| Header | Description |
| ---------------- | ------------------------------------------------------------ |
| X-Auth-Apikey | Access Key for API key (see 'How to create API key section ?') |
| X-Auth-Nonce | Timestamp in milliseconds (can be passed as a string) |
| X-Auth-Signature | HMAC-SHA256, calculated using concatenation of X-Auth-Nonce and Access Key |
1. Generate X-Auth-Nonce - unique string (e.g current unix timestamp)
```bash
date +%s%3N
1584524005143
```
Nonce will be validated on server side to be not older than 5 seconds from the generation moment
2. Calculate X-Auth-Signature header.
X-Auth-Signature is HMAC-SHA256, calculated using concatenation of X-Auth-Nonce and Access Key.
```ruby
nonce = (Time.now.to_f * 1000).to_i.to_s # timestamp in milliseconds, ex: 1584524005143
access_key = '61d025b8573501c2' # Access Key from 'How to create API key section ?'
secret_key = '2d0b4979c7fe6986daa8e21d1dc0644f' # Secret Key from 'How to create API key section ?'
OpenSSL::HMAC.hexdigest("SHA256", secret_key, nonce + access_key)
# => "bd42b945e095880e28d046846dbecf655fdf09d95a396a24fe6fe1df42f15d13"
```
3. Pass your headers in httpie (note `--session` is not needed anymore)
```
http https://your.domain.com/api/v2/peatio/account/balances \
"X-Auth-Apikey: 61d025b8573501c2" \
"X-Auth-Nonce: 1584524005143" \
"X-Auth-Signature: bd42b945e095880e28d046846dbecf655fdf09d95a396a24fe6fe1df42f15d13"
```

48
docs/general/auth0.md Normal file
View File

@@ -0,0 +1,48 @@
# Auth0 integration
## How to create an application
When you signed up for Auth0, a new application was created for you, or you could have created a new one (the most appropriate application for our structure is Single Page Application).
![Application](../images/auth0_dashboard.png)
You will need some details about that application to communicate with Auth0. You can get these details from the Application Settings section in the Auth0 dashboard.
![Settings](../images/auth0_settings.png)
You should put `auth0_domain` from the Domain field and `auth0_client_id` from the Client ID field
For a single page application better to use authorization code flow with proof key for code exchange.
## Authorization Code Flow with Proof Key for Code Exchange (PKCE)
When public clients request Access Tokens, some additional security concerns are posed that are not mitigated by the Authorization Code Flow alone. This is because single-page apps cannot securely store a Client Secret because their entire source is available to the browser.
### How it works
![PKCE](../images/auth0_pkce.png)
Because the PKCE-enhanced Authorization Code Flow builds upon the standard Authorization Code Flow, the steps are very similar.
1. The user clicks Login within the application.
2. Auth0's SDK creates a cryptographically-random code_verifier and from this generates a code_challenge.
3. Auth0's SDK redirects the user to the Auth0 Authorization Server (/authorize endpoint) along with the code_challenge.
4. Your Auth0 Authorization Server redirects the user to the login and authorization prompt.
5. The user authenticates using one of the configured login options and may see a consent page listing the permissions Auth0 will give to the application.
6. Your Auth0 Authorization Server stores the code_challenge and redirects the user back to the application with an authorization code, which is good for one use.
7. Auth0's SDK sends this code and the code_verifier (created in step 2) to the Auth0 Authorization Server (/oauth/token endpoint).
8. Your Auth0 Authorization Server verifies the code_challenge and code_verifier.
9. Your Auth0 Authorization Server responds with an ID Token and Access Token (and optionally, a Refresh Token).
10. Your application can use the Access Token to call an API to access information about the user.
11. The API responds with requested data.
You can try to call your [API using the authorization Code Flow with PKCE](https://auth0.com/docs/flows/call-your-api-using-the-authorization-code-flow-with-pkce).
Also you can find a link with [Authentication API description](https://auth0.com/docs/api/authentication#introduction) here.

32
docs/general/captcha.md Normal file
View File

@@ -0,0 +1,32 @@
# Barong Captcha Policy
#### Overview
A CAPTCHA (an acronym for "Completely Automated Public Turing test to tell Computers and Humans Apart") is a type of challengeresponse test used in computing to determine whether or not the user is human) [Link to wiki](https://en.wikipedia.org/wiki/CAPTCHA)
Currently Barong versions 2.3+ supports 3 options in captcha policy on `sign up` and `sign in` API endpoints.
Configuration manages through environment variable - `BARONG_CAPTCHA`. Available values - `geetest`, `recaptcha`, `none`.
With a wrong value barong will fail on start with error: `#{KEY} invalid, enabled values: NONE GEETEST RECAPTCHA`.
## Disabled (default)
`none` - if ENV `BARONG_CAPTCHA` has this value - no captcha response will be required on sign in and sign up, so no bot traffic prevention.
This option is not recommended to use in `production` environment.
`None` policy was designed in testing and demo purposes, to start barong without any additional keys.
## Re CAPTCHA v2
reCAPTCHA is a free service that protects your site from spam and abuse. It uses advanced risk analysis techniques to tell humans and bots apart. [Get started from google team](https://developers.google.com/recaptcha/intro)
`recaptcha` - this value in `BARONG_CAPTCHA` env enables re_captcha protection, designed and maintained by Google company. [Small developers tips from google team](https://developers.google.com/recaptcha/docs/display)
To properly configurate re_captcha you will need to set value for ENVs `recaptcha_site_key` and `recaptcha_secret_key`. Both of them you can generate [in google admin panel](https://www.google.com/recaptcha/admin/create)
After enabling and configuring captcha, `sign up` and `sign in` endpoint will require new parameter - `captcha_response`(`string`) and validate captcha response on server side, to protect from bots traffic.
## Geetest Captcha (Puzzle captcha)
GeeTest captcha is an user-friendly captcha with high security. GeeTest captcha enables digital businesses to secure control of their websites against bots. [geetest captcha site](https://www.geetest.com)
`geetest` - this value in BARONG_CAPTCHA env enables geetest captcha protection, designed and maintained by geetest.com
To properly configurate `geetest` you will need to set value for ENVs `geetest_id` and `geetest_key`. How to generate them, you can find in official [get started guide](https://docs.geetest.com/captcha/overview/guide)
After enabling and configuring geetest captcha, `sign up` and `sign in` endpoint will require new parameter - `captcha_response`(`hash` - with three keys `geetest_challenge`, `geetest_seccode`, `geetest_validate`) and validate captcha response on server side, to protect from bots traffic.

View File

@@ -0,0 +1,46 @@
## Encryption
Data sensitivity will be defined as follow:\
**Low**: IP address\
**Medium**: Email address, Location data\
**High**: Full name, Street address, phone number, date of birth\
**Very High**: Passport number, Drivers license number
Low and Medium will not be masked on the UI.
| Field | Mask | Comment |
|---|---|---|
| Street address | No mask | No need |
| First Name | No mask | No need |
| Last Name | B****** | Display first letter |
| Phone Number | +380 **** 4556 | Display country code and last 4 digits |
| Date of birth | 1980-01-** | Hide day |
| Document Number | FG****64 | First 2 number and last 2 digits |
### Approach
Rails offers a handy ActiveSupport::MessageEncryptor class, that hides away all the complexity of data encryption, and was wrapped in a simple to use service object or reusable module.
Service object class doing the actual heavy lifting, but only exposing two straightforward public class methods encrypt and decrypt.
System have weekly salt rotation, so encrypted keys in DB will be prepended with salt, which will be mix of year and week number starting from 0.
Make sure to store `SECRET_KEY_BASE` somewhere safe otherwise, you would not be able to decrypt your secure data, also you need to have this ENV variable at the start of your application as it will not create models (Profile, Phone, Documents) which have encrypted fields.
#### Searching by encrypted values
To have ability to search by encrypted fields, system implements additional field named `attribute_index` which use [crc32 algorithm](http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html) for storing attribute value.
Make sure you have `BARONG_CRC32_SALT` to make algrithm more powerful.
#### Rotation
To update all encrypted fields to latest key values (salt will be "#{current_year}#{current_week}"), you can use following rake tasks:
`rake rotate:phones`
`rake rotate:profiles`
`rake rotate:documents`
### Fields masking on user API
Sensitive data fields like `last name`, `dob`, `phone number`, `document number` are masked in user API by default.
You can disable this masking by changing the environment variable `BARONG_API_DATA_MASKING_ENABLED` to `false`.

179
docs/general/errors.md Normal file
View File

@@ -0,0 +1,179 @@
# Barong errors list
## Resource module errors
```
resource.labels.private - Can't update Label.
resource.user.no_activity No activity recorded or wrong topic
resource.user.empty_otp_code Cant delete account. 2FA is on, but otp_code is empty
resource.user.invalid_otp Cant delete account. 2FA is on, but otp_code is invalid
resource.user.missing_otp_code Cant delete account. 2FA is on, but otp_code is missing
resource.user.invalid_password Cant delete account. Password is wrong
resource.profile.not_exist User has no profile
resource.profile.exist Profile already exists
resource.api_key.2fa_disabled Only accounts with enabled 2FA alowed
resource.api_key.missing_otp Theaccount has enabled 2FA but OTP code is missing
resource.api_key.invalid_otp OTP code is invalid
resource.phone.twillio Something wrong with Twilio Client
resource.phone.invalid_num Phone number is invalid
resource.phone.number_exist Phone number already exists
resource.phone.verification_invalid Phone is not found or verification code is invalid
resource.documents.limit_reached Maximum number of documents already reached
resource.documents.limit_will_be_reached Documents amount will reach limit by this upload
resource.otp.already_enabled 2FA has been already enabled for this account
resource.otp.invalid OTP code is invalid
resource.password.doesnt_match New passwords don\'t match
resource.password.prev_pass_not_correct Previous password is not correct
resource.password.no_change_provided New password cant be the same, as old one
```
## Identity module errors
```
identity.user.invalid_referral_format Invalid referral uid format
identity.user.referral_doesnt_exist Referral doesn't exist
identity.user.active_or_doesnt_exist User doesn't exist or has already been activated'
identity.password.user_doesnt_exist User doesn't exist
identity.user.passwords_doesnt_match Passwords don't match
identity.user.utilized_token JWT has already been used
identity.session.invalid_params Invalid Email or Password
identity.session.invalid Invalid Session
identity.captcha.required captcha_response is required
identity.captcha.mandatory_fields Mandatory fields must be filled in
identity.session.deleted Your account is deleted
identity.session.not_active Your account is not active
identity.session.banned Your account is banned
identity.session.invalid_params Invalid Email or Password
identity.session.missing_otp The account has enabled 2FA but OTP code is missing
identity.session.invalid_otp OTP code is invalid
```
## Admin module errors
```
admin.user.update_himself Admin can't update himself
admin.user.enable_2fa Manual 2FA enabling not allowed
admin.user.state_no_change Can't change state, as its already {active}
admin.user.doesnt_exist User with such UID doesnt exist
admin.label.doesnt_exist Label with such key doesnt exist or not assigned to chosen user
admin.access.denied Access Denied: User is not Admin
admin.user.non_user_field Search field is not a user attribute
admin.user.no_matches Search result is empty array
admin.user.label_no_matches Search result is empty array
```
## General errors
```
record.not_found Record is not found
jwt.decode_and_verify Failed to decode and verify JWT
authz.invalid_session Failed to decode cookies
authz.user_not_active User is not active
authz.invalid_signature API Key header 'signature' is invalid
authz.apikey_not_active API Key state is 'inactive'
authz.disabled_2fa API Key owner has disabled 2FA
authz.invalid_api_key_headers Blank or missing API Key headers
authz.permission_denied Path is blacklisted
authz.unexistent_apikey X-Auth-Apikey header is invalid
```
## Validation errors
### Admin module
```
admin.user.non_integer_page
admin.user.non_positive_page
admin.user.non_integer_limit
admin.user.invalid_limit
admin.user.missing_uid
admin.user.empty_uid
admin.user.empty_state
admin.user.empty_otp
admin.user.empty_role
admin.user.one_of_state_otp
admin.user.one_of_state_otp_email
admin.user.missing_key
admin.user.empty_key
admin.user.missing_scope
admin.user.empty_scope
admin.user.missing_value
admin.user.empty_value
```
### Identity module
```
identity.user.missing_email
identity.user.empty_email
identity.user.missing_password
identity.user.empty_password
identity.user.missing_token
identity.user.empty_token
identity.user.missing_reset_password_token
identity.user.empty_reset_password_token
identity.user.missing_confirm_password
identity.user.empty_confirm_password
identity.session.missing_email
identity.session.missing_password
identity.session.invalid_captcha_format
```
### Resource module
```
resource.otp.missing_code
resource.otp.empty_code
resource.labels.missing_key
resource.labels.empty_key
resource.labels.missing_value
resource.labels.empty_value
resource.documents.expire_not_a_date
resource.documents.invalid_format
resource.documents.already_expired
resource.documents.missing_doc_expire
resource.documents.empty_doc_expire
resource.documents.missing_doc_type
resource.documents.empty_doc_type
resource.documents.missing_doc_number
resource.documents.empty_doc_number
resource.documents.missing_upload
resource.user.missing_topic
resource.user.empty_topic
resource.user.missing_old_password
resource.user.empty_old_password
resource.user.missing_new_password
resource.user.empty_new_password
resource.user.missing_confirm_password
resource.user.empty_confirm_password
resource.profile.missing_first_name
resource.profile.missing_last_name
resource.profile.missing_dob
resource.profile.missing_address
resource.profile.missing_postcode
resource.profile.missing_city
resource.profile.missing_country
resource.api_key.missing_algorithm
resource.api_key.empty_algorithm
resource.api_key.empty_kid
resource.api_key.empty_scope
resource.api_key.missing_totp
resource.api_key.empty_totp
resource.api_key.missing_kid
resource.api_key.empty_state
resource.phone.missing_phone_number
resource.phone.empty_phone_number
resource.phone.missing_verification_code
resource.phone.empty_verification_code
```

593
docs/general/event_api.md Normal file
View File

@@ -0,0 +1,593 @@
# RabbitMQ Barong Event API
## Overview of RabbitMQ details
Barong submits all events into three exchanges depending on event category (read next).
The exchange name consists of three parts:
1) application name (typically `barong`)
2) fixed keyword `events`.
3) category of event, like `system` (generic system event), `model` (the attributes of some record were updated)
The routing key looks like `user.password.reset.token`, `user.created`.
The event name matches the routing key but with event category appended at the beginning, like `system.user.password.reset.token`, `market.user.created`.
## Overview of RabbitMQ message
Each produced message in `Event API` is JWT (complete format).
This is very similar to `Management API`.
The example below demonstrates both generation and verification of JWT:
```ruby
require "jwt-multisig"
require "securerandom"
jwt_payload = {
iss: 'barong',
jti: SecureRandom.uuid,
iat: Time.now.to_i,
exp: Time.now.to_i + 60,
event: event_payload
}
private_key = OpenSSL::PKey.read(Base64.urlsafe_decode64(private_key)
algorithm = 'RS256'
jwt = JWT::Multisig.generate_jwt jwt_payload, \
{ barong: private_key },
{ barong: algorithm }
Kernel.puts "GENERATED JWT", jwt.to_json, "\n"
verification_result = JWT::Multisig.verify_jwt jwt.deep_stringify_keys, \
{ barong: public_key }, { verify_iss: true, iss: "barong", verify_jti: true }
decoded_jwt_payload = verification_result[:payload]
Kernel.puts "MATCH AFTER VERIFICATION: #{jwt_payload == decoded_jwt_payload}."
```
The RabbitMQ message is stored in JWT field called `event`.
## Overview of Event API message
The typical event looks like (JSON):
```ruby
event: {
record: {
foo: "ID30DD0DD986",
bar: "example@barong.io",
baz: "member",
qux: 0
},
name: "model.user.created"
}
```
The field `event[:name]` contains event name (same as routing key).
The fields `foo`, `bar`, `baz`, `qux` (example) are fields which carry useful information.
# Barong Event API messages
## Format of `model.user.created` event
```ruby
event: {
record: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "pending",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
},
name: "model.user.created"
}
```
| Field | Description |
| ---------- | ----------------------------------- |
| `record` | Created user up-to-date attributes. |
## Format of `model.user.updated` event
```ruby
event: {
record: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 1,
otp: false,
state: "pending",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
},
changes: {
level: 0
}
name: "model.user.updated"
}
```
| Field | Description |
| ---------- | ----------------------------------- |
| `record` | Created user up-to-date attributes. |
| `changes` | The changed user attributes and their values. |
## Format of `model.user.created` event
```ruby
event: {
record: {
address:"Illinois",
city:"New Garfieldbury",
country:"COD",
dob:"1984-05-22",
first_name:"Irina",
last_name:"Heathcote",
postcode:"10029",
created_at:"2019-10-02T08:14:20Z",
updated_at:"2019-10-02T08:14:20Z",
user: {
email: "clarisa_larkin@sawayn.info",
level: 0,
otp: false,
referral_uid: nil,
role: "member",
state: "pending",
uid: "IDEA819FB3F1",
updated_at: "2019-10-02T08:14:20Z"
created_at: "2019-10-02T08:14:20Z"
}
},
name: "model.profile.created"
}
```
| Field | Description |
| ---------- | -------------------------------------- |
| `record` | Created profile up-to-date attributes. |
## Format of `model.user.updated` event
```ruby
event: {
record: {
address:"Illinois",
city:"New Garfieldbury",
country:"COD",
created_at:"2019-10-02T08:14:20Z",
dob:"1984-05-22",
first_name:"Irina",
last_name:"Heathcote",
postcode:"10029",
updated_at:"2019-10-02T08:14:20Z",
user: {
email: "clarisa_larkin@sawayn.info",
level: 0,
otp: false,
referral_uid: nil,
role: "member",
state: "pending",
uid: "IDEA819FB3F1",
updated_at: "2019-10-02T08:14:20Z"
created_at: "2019-10-02T08:14:20Z"
}
},
changes: {
first_name: "Vernell"
},
name: "model.profile.updated"
}
```
| Field | Description |
| ---------- | ----------------------------------- |
| `record` | Profile up-to-date attributes. |
| `changes` | The changed profile attributes and their values. |
## Format of `model.label.created` event
```ruby
event: {
record: {
id: 1,
key: "email",
value: "verified",
user: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 2,
otp: false,
state: "active",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
}
},
name: "model.label.created"
}
```
| Field | Description |
| ---------- | ----------------------------------- |
| `record` | Created label up-to-date attributes. |
## Format of `model.label.updated` event
```ruby
event: {
record: {
id: 1,
key: "new_key",
value: "verified",
user: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 2,
otp: false,
state: "active",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
}
},
changes: {
key: "old_key"
}
name: "model.label.updated"
}
```
| Field | Description |
| ---------- | ----------------------------------- |
| `record` | Created label up-to-date attributes. |
| `changes` | The changed label attributes and their values. |
## Format of `model.document.created` event
```ruby
event: {
record: {
doc_type: 'Passport',
doc_expire: '3020-01-22',
doc_number: 'AA1234BB',
upload: [],
updated_at:"2019-01-28T08:35:29Z",
created_at:"2019-01-28T08:35:29ZZ",
user: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 2,
otp: false,
state: "active",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
}
}
name: "model.document.created"
}
```
| Field | Description |
| ------------ | --------------------------------- |
| `user` | The up-to-date user attributes. |
| `doc_type` | Document type. |
| `doc_expire` | Experation time for document. |
| `doc_number` | Document number. |
| `upload` | Array of updaded objects |
| `updated_at` | Time of document object creation |
| `created_at` | Time of last document update |
## Format of `system.user.email.confirmation.token` event
```ruby
event: {
record: {
user: {
uid: "ID739065AFD3",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "pending",
created_at: "2019-01-28T09:03:50Z",
updated_at: "2019-01-28T09:03:50Z"
},
language: "EN",
domain: "www.barong.io",
token: "eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NDg2NjYyMzAsImV4cCI6MTU0ODY3MjIzMCwic3ViIjoiY29uZmlybWF0aW9uIiwiaXNzIjoiYmFyb25nIiwiYXVkIjpbInBlYXRpbyIsImJhcm9uZyJdLCJqdGkiOiI5OWJkNzFkMjU2NTdlMmI1YzI1MCIsImVtYWlsIjoiYWRtaW4xMjNAYmFyb25nLmlvIiwidWlkIjoiSUQ3MzkwNjVBRkQzIn0.OI5tL9kV6cA1JBAy7G5iqd3WplxcB-waHYKFjm83koMEpx2Hlw9fksq5lip5cIHTjR8i3ambFL40OaCwDNc1jAiDsHwuv2nLswgi88_M1G8KVFylboQdtgmH_cZiz-Y-51Fq2oqEID5QyJnsSMSJbfspb6A0JGT_V-SPK4WFZw43F_RKhlZBCrxojljMwd20rGqFPYirMgUpsfiW0_-mESXzQ7UK1eA8mYO7Id4y6JR2Yoo-JTloEnBL1M189tOz6LqmmQB0M_QjTiHG3y9I97Med3StgVziYo9qog9kJXyPuXbboddg__5WEhMcWbaToohoiT5UvpVJHKfgxEVaDg"
},
name: "system.user.email.confirmation.token"
}
```
| Field | Description |
| ---------- | ------------------------------------------------ |
| `user` | The up-to-date user attributes. |
| `language` | The language. |
| `domain` | The domain name of barong. |
| `token` | Valid confirm-acc jwt token (mandatory param for user confirmation endpoint) `/identity/users/email/confirm_code`. |
## Format of `system.user.email.confirmed` event
```ruby
event: {
record: {
user: {
uid: "IDB1629BFE9E",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "active",
created_at: "2019-01-28T10:17:27Z",
updated_at: "2019-01-28T10:17:45Z"
},
language: "EN",
domain: "www.barong.io"
},
name: "system.user.email.confirmed"
}
```
| Field | Description |
| ---------- | -------------------------------- |
| `user` | The up-to-date user attributes. |
| `language` | The language. |
| `domain` | The domain name of barong. |
## Format of `system.user.password.reset.token` event
```ruby
event: {
record: {
user: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "pending",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
},
language: "EN",
domain: "www.barong.io",
token: "eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NDg2NjQ1OTUsImV4cCI6MTU0ODY3MDU5NSwic3ViIjoicmVzZXQiLCJpc3MiOiJiYXJvbmciLCJhdWQiOlsicGVhdGlvIiwiYmFyb25nIl0sImp0aSI6IjRhY2IzM2IzYmE2NDc0ZjY1YTI5IiwiZW1haWwiOiJhZG1pbjEyQGJhcm9uZy5pbyIsInVpZCI6IklEMzBERDBERDk4NiJ9.Rie4LCbkV0jVBbhMoceYx8a9uDA-ea9D1v790zlIqP_EY8Iue_OOKXYWiC1Y-55MPicFbknBILjZlPewvAF8ZrhqIt04ROsgBdDGEUGY_SnLWhXzqSx9-v_o_w2MVjLOUxvRBm6sD0RvL-_5LmOcLqhYtf7ZPUnPDwsvhDedqDfbXPEvI7OK2SZ-1uPAOg1IMOX1k7xaDt5I1Wp-Knr2DmEgwNYbIjaXraComYcMdtVSuYVJAufgA0kTADMeT3cV3jzGy9dNfs8heMCtf5tr72IbL0_N0VeUQj9uaPDUr4ntsYk7gOPmA3RSVrSismtYdBXA9oLA0b0YfOctiY9dqg"
},
name: "system.user.password.reset.token"
}
```
| Field | Description |
| ---------- | ------------------------------------------------ |
| `user` | The up-to-date user attributes. |
| `language` | The language. |
| `domain` | The domain name of barong. |
| `token` | Valid reset-pass jwt token (mandatory param for password reset endpoint) `/identity/users/password/confirm_code`. |
## Format of `system.user.account.deleted` event
```ruby
event: {
record: {
user: {
uid: "IDB1629BFE9E",
email: "example@barong.io",
role: "member",
level: 1,
otp: false,
state: "deleted",
created_at: "2019-01-28T10:17:27Z",
updated_at: "2019-01-28T10:17:45Z",
}
},
name: "system.user.account.deleted"
}
```
| Field | Description |
| ---------- | ------------------------------------------------ |
| `user` | The up-to-date user attributes. |
## Format of `system.user.password.reset` event
```ruby
event: {
record: {
user: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "pending",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T09:42:36Z"
}
},
name: "system.user.password.reset"
}
```
| Field | Description |
| ---------- | ------------------------------------------------ |
| `user` | The up-to-date user attributes. |
## Format of `system.user.password.change` event
```ruby
event: {
record: {
user: {
uid: "IDC554ED1D0F",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "active",
created_at: "2019-01-09T15:54:56Z",
updated_at: "2019-01-28T09:59:03Z"
}
},
name: "system.user.password.change"
}
```
| Field | Description |
| --------- | ------------------------------------------------- |
| `user` | The up-to-date user attributes. |
## Format of `system.document.verified` event
```ruby
event: {
record: {
user: {
uid: "IDC554ED1D0F",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "active",
created_at: "2019-01-09T15:54:56Z",
updated_at: "2019-01-28T09:59:03Z"
},
id: 1,
key: "something",
value: "verified"
},
name: "system.document.verified"
}
```
| Field | Description |
| --------- | ------------------------------------------------- |
| `user` | The up-to-date user attributes. |
## Format of `system.document.rejected` event
```ruby
event: {
record: {
user: {
uid: "IDC554ED1D0F",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "active",
created_at: "2019-01-09T15:54:56Z",
updated_at: "2019-01-28T09:59:03Z"
},
id: 1,
key: "something",
value: "rejected"
},
name: "system.document.rejected"
}
```
| Field | Description |
| --------- | ------------------------------------------------- |
| `user` | The up-to-date user attributes. |
## Format of `system.session.create` event
```ruby
event: {
record: {
user: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "pending",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
},
user_ip: "127.0.0.1",
user_agent: "Chrome"
},
name: "system.session.create"
}
```
## Producing events using Ruby
```ruby
require "bunny"
def generate_jwt(jwt_payload)
Kernel.abort "Please, see «Overview of RabbitMQ message» for implementation guide."
end
Bunny.run host: "localhost", port: 5672, username: "guest", password: "guest" do |session|
channel = session.channel
exchange = channel.direct("barong.events.model")
jwt_payload = {
iss: "barong",
jti: SecureRandom.uuid,
iat: Time.now.to_i,
exp: Time.now.to_i + 60,
event: {
record: {
uid: "ID30DD0DD986",
email: "example@barong.io",
role: "member",
level: 0,
otp: false,
state: "pending",
created_at: "2019-01-28T08:35:29Z",
updated_at: "2019-01-28T08:35:29Z"
},
name: "model.user.created"
}
}
exchange.publish(generate_jwt(jwt_payload), routing_key: "user.created")
end
```
IMPORTANT: Don't forget to implement the logic for JWT exception handling!
## Producing events using `rabbitmqadmin`
`rabbitmqadmin publish routing_key=user.created payload=JWT exchange=barong.events.model`
Don't forget to pass environment variable `JWT`.
## Consuming events using Ruby
```ruby
require "bunny"
def verify_jwt(jwt_payload)
Kernel.abort "Please, see «Overview of RabbitMQ message» for implementation guide."
end
Bunny.run host: "localhost", port: 5672, username: "guest", password: "guest" do |session|
channel = session.channel
exchange = channel.direct("barong.events.model")
queue = channel.queue("", auto_delete: true, durable: true, exclusive: true)
.bind(exchange, routing_key: "user.created")
queue.subscribe manual_ack: true, block: true do |delivery_info, metadata, payload|
Kernel.puts verify_jwt(JSON.parse(payload)).fetch(:event)
channel.ack(delivery_info.delivery_tag)
rescue => e
channel.nack(delivery_info.delivery_tag, false, true)
end
end
```
IMPORTANT: Don't forget to implement the logic for JWT exception handling!

View File

@@ -0,0 +1,71 @@
## Barong password hashing ##
### Overview ###
Barong since 2.0 version use OpenBSD bcrypt() password hashing algorithm, that allow us easily store a secure hash of users' passwords.
As a base Barong takes [bcrypt-ruby gem](https://github.com/codahale/bcrypt-ruby) - Ruby binding for the OpenBSD bcrypt()
With [rails 5 has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html) it gives us full power of algorithm
### How it works ###
Hash algorithms take a chunk of data (e.g., user's password) and create a "digital fingerprint," or hash, of it.
Because this process is not reversible, there's no way to go from the hash back to the password.
In other words:
hash(p) #=> <unique gibberish>
We store the hash and check it against a hash made of a potentially valid password:
<unique gibberish> =? hash(just_entered_password)
### Rainbow Tables
But even this has weaknesses -- attackers can just run lists of possible passwords through the same algorithm, store the
results in a big database, and then look up the passwords by their hash:
PrecomputedPassword.find_by_hash(<unique gibberish>).password #=> "secret1"
Our solution to this is to add a small chunk of random data -- called a salt -- to the password before it's hashed:
hash(salt + p) #=> <really unique gibberish>
The salt is then stored along with the hash in the database, and used to check potentially valid passwords:
<really unique gibberish> =? hash(salt + just_entered_password)
bcrypt-ruby automatically handles the storage and generation of these salts for you.
Adding a salt means that an attacker has to have a gigantic database for each unique salt -- for a salt made of 4
letters, that's 456,976 different databases. Pretty much no one has that much storage space, so attackers try a
different, slower method -- throw a list of potential passwords at each individual password:
hash(salt + "aadvark") =? <really unique gibberish>
hash(salt + "abacus") =? <really unique gibberish>
etc.
This is much slower than the big database approach, but most hash algorithms are pretty quick -- and therein lies the
problem. Hash algorithms aren't usually designed to be slow, they're designed to turn gigabytes of data into secure
fingerprints as quickly as possible. `bcrypt()`, though, is designed to be computationally expensive:
Ten thousand iterations:
user system total real
md5 0.070000 0.000000 0.070000 ( 0.070415)
bcrypt 22.230000 0.080000 22.310000 ( 22.493822)
If an attacker was using Ruby to check each password, they could check ~140,000 passwords a second with MD5 but only
~450 passwords a second with `bcrypt()`.
## More Information
`bcrypt()` is currently used as the default password storage hash in OpenBSD, widely regarded as the most secure operating
system available.
For a more technical explanation of the algorithm and its design criteria, please read Niels Provos and David Mazières'
Usenix99 paper:
https://www.usenix.org/events/usenix99/provos.html
If you'd like more down-to-earth advice regarding cryptography, I suggest reading <i>Practical Cryptography</i> by Niels
Ferguson and Bruce Schneier:
https://www.schneier.com/book-practical.html

45
docs/general/profiles.md Normal file
View File

@@ -0,0 +1,45 @@
# Barong
## Profiles story and administration
This document explain original profiles submit-n-verify process and possible customizations.
## Version
Story described in the document actual for latest 2.5 stable version and higher.
## User side of the story
`Comment`: Previously (in 2.3 and lower) user was able to submit only 1 profile, and all later modifications affect it. Starting from 2.4 we changed `user has_one profile` relation to `user has many profiles`. This was done first of all to be able to track history of modifications and to be able to control changes from admin panel. Meanwhile, it also brought additional manual verification step in the legacy KYC process.
`Story`:
User can submit profile with following fields (all are optional by default): (via `POST /resource/profiles`)
```
t.string "first_name"
t.string "last_name"
t.date "dob"
t.string "address"
t.string "postcode"
t.string "city"
t.string "country"
t.text "metadata"
```
Profile creates with `drafted` state in database. At this point user can edit the information (via `PUT /resource/profiles`), and administrators will not review it yet.
Once all the information is edited and validated by user correctly, he can submit profile for verification (via `PUT /resource/profiles`) by passing `confirmation: true` in the params. Profile state changes to "submitted" in database and from now on this profile is pending for admin verification.
Meanwhile, there is a possibility to skip this "edit" step and create a profile directly with `submitted` state. For this user need to pass `confirmation: true` parameter directly in `POST /resource/profiles`.
After admin will verify the profile and mark it as `"verified"` or `"rejected"` user will be able to create a new profile with `drafted/submitted` state, if he need it. Flow mostly controls by a `server-side rule`: user can have `ANY` amount of profiles, but `ONLY ONE` of `drafted/submitted` at a time.
## Admin side of the story
Once user sumbit a profile (with state submitted) admin can verify or reject it, by changing a profile state and creating a correct label. Usually its `key: profile, value: verified/rejected'.
Also administrator has an access to the full profiles history, so he can check and compare new changes with old profiles, if the exist. As well he has an information about previous decisions about profile verification per each request.
Once profile is rejected or verified, admin can create new profile for user (via `POST admin/profiles`).
In this case, profile will have a 'submitted' state and 'author' field with admin UID in DB.
`!!!Attention` By default, if admin creates a profile for user, the same admin account cant approve or reject this profile, he need to wait for second admin approval. However, this can be changed by env `BARONG_PROFILE_DOUBLE_VERIFICATION` which can receive 2 value: `true` for enabling and `false` for disabling the feature