JWS Tokens
The backend uses JWS tokens (encrypted data structures using public/private keys) to allow transmission of data between servers by a client, while protecting this data from alteration.
Create a new type of token
First, define your token data structure in app/payloads/yourtype_token.go
:
// ThreadToken represents data inside a thread token.
type ThreadToken struct {
Date string `json:"date" validate:"dmy-date"` // dd-mm-yyyy
...
ItemID string `json:"item_id"`
...
PublicKey *rsa.PublicKey
PrivateKey *rsa.PrivateKey
}
Date
is required by the token system. When the token is encrypted, it will automatically add the current date time.Now
in this field. Additionally, when a token is decrypted by the backend, Date
must be between yesterday and tomorrow, otherwise the token will be rejected. This must be taken care of when testing tokens.
Note: use string
to store int
values, otherwise you’ll run into type issues.
Next, define the methods to marshall/unmarshal your token in app/token/
. Use the existing examples.
To get a token from the defined data structure:
threadToken, err := (&token.Thread{
ItemID: strconv.FormatInt(itemID, 10),
...
}).Sign(srv.TokenConfig.PrivateKey)
Testing tokens
Unit tests
For unit tests, a specific public and private key defined in tokentest/sample_keys.go
are used.
To generate a token using those keys, you can use the function generateSignedTestToken
defined in token/token_test.go
. It will allow you to create a token with the information you want inside.
Whenever you decrypt a token in your test (unmarshal), be sure to first pass the time.Now
, otherwise it will get rejected because the date doesn`t match:
monkey.Patch(time.Now, func() time.Time { return timeWhenTheTokenWasGenerated })
Integration tests (Gherkin)
Since different keys are used in the local and CircleCI test environments, the integrations can’t test the encrypted tokens directly.
Instead, you need to use the following Gherkin feature:
And the response body decoded as "ThreadTokenResponse" should be, in JSON:
"""
{
"participant_id": 1,
...
"token": {
"can_watch": false,
...
}
}
"""
This allows you to test directly the decrypted content of the token.
Note that the type ThreadTokenResponse
has to be defined. The is currently done in testhelpers/known_types.go
:
type threadGetResponse struct {
ItemID int64 `json:"item_id"`
...
ThreadToken token.Thread `json:"token"`
PublicKey *rsa.PublicKey
}
type threadGetResponseWrapper struct {
ItemID int64 `json:"item_id"`
...
ThreadToken *string `json:"token"`
}
func (resp *threadGetResponse) UnmarshalJSON(raw []byte) error {
wrapper := threadGetResponseWrapper{}
if err := json.Unmarshal(raw, &wrapper); err != nil {
return err
}
resp.ItemID = wrapper.ItemID
...
if wrapper.ThreadToken != nil {
resp.ThreadToken.PublicKey = resp.PublicKey
return (&resp.ThreadToken).UnmarshalString(*wrapper.ThreadToken)
}
return nil
}
...
var knownTypes = map[string]reflect.Type{
...
"ThreadTokenResponse": reflect.TypeOf(&threadGetResponse{}).Elem(),
}
Note the type difference between threadGetResponse
and threadGetResponseWrapper
. The first one contains the decrypted data, while the second one contains it as a string token.
All the other fields between those two structures are common. It would be a good idea to define them once instead of duplicating them, but this is currently not possible due to a limitation in app/payloads/common.go:ConvertIntoMap
. It should also be possible to remove the PublicKey
from threadGetResponse
to make the design clearer.