Module Description
This module implements handling of tokens in an OAuth-like authentication scheme. It provides services necessary to:
- deserialize/serialize binary tokens received and issued by the server,
- validate incoming binary tokens, i.e.:
- check integrity using Message Authentication Codes (MAC) with server-side stored user keys,
- check validity against the configured validity duration times,
- check revocation status,
- handle token requests from logged in users.
The module itself does not implement protocol related details - these are implemented in cyrsasl.erl
.
Generation of keys necessary to sign binary tokens is delegated to module mod_keystore.erl
.
Options
Validity periods
Validity periods of access and refresh tokens can be defined independently.
Allowed units are:
- days
- hours
- minutes
- seconds
The default values for tokens are:
- 1 hour for an access token
- 25 days for a refresh token
Example configuration from mongooseim.cfg
, inside modules
section:
{modules, [
{mod_auth_token, [{{validity_period, access}, {13, minutes}},
{{validity_period, refresh}, {13, days}}]
]}.
Validity period configuration for provision tokens happens outside the module since the server does not generate provision tokens - it only validates them.
Required keys
Keys are used for signing binary tokens using an HMAC with SHA-2 family function SHA-384.
Therefore, mod_auth_token
requires mod_keystore
to provide some predefined keys.
The required keys are (example from mongooseim.cfg
):
{mod_keystore, [{keys, [{token_secret, ram},
{provision_pre_shared, {file, "priv/provision_pre_shared.key"}}]}]}
token_secret
is a RAM-only (i.e. generated on cluster startup, never written to disk) key used for signing and verifying access and refresh tokens.
provision_pre_shared
is a key read from a file.
As its name suggests, it's shared with a service issuing provision tokens.
Clients then use these provision tokens to authenticate with MongooseIM.
While it's not enforced by the server and left completely to the operator, provision_pre_shared
keys probably should not be shared between virtual XMPP domains hosted by the server.
That is, make sure the module configuration specifying a provision_pre_shared
key is specific to an XMPP domain.
MongooseIM can't generate provision tokens on its own (neither can it distribute them to clients), so while configuring a provision_pre_shared
key to be RAM-only is technically possible, it would in practice disable the provision token support (as no external service could generate a valid token with this particular RAM key).
Token types
Three token types are supported:
-
access tokens: These are short lived tokens which grants aren't tracked by the server (i.e. there's no need to store anything in a database). Access tokens can be used as a payload for the X-OAUTH authentication mechanism and grant access to the system. Access tokens can't be revoked. An access token is valid only until its expiry date is reached.
-
refresh tokens: These are longer lived tokens which are tracked by the server and therefore require persistent storage (as of now only PostgreSQL is supported). Refresh tokens can be used as a payload for the X-OAUTH authentication mechanism and to grant access to the system. Also they can result in a new set of tokens being returned upon successful authentication. They can be revoked - if a refresh token hasn't been revoked, it is valid until it has expired. On revocation, it immediately becomes invalid. As the server stores information about granted tokens, it can also persistently mark them as revoked.
-
provision tokens: These tokens are generated by a service external to the server. They grant the owner a permission to create an account. A provision token may contain information which the server can use to provision the VCard for the newly created account. Using a provision token to create an account (and inject VCard data) is done similarly to other token types, i.e. by passing it as payload for the X-OAUTH mechanism. The XMPP server has no way of tracking and revoking provision tokens, as they come from an outside source.
Token serialization format
All tokens (access, refresh, provision) are to be exchanged as Base64 encoded binary data. Serialization format of the token before encoding with Base64 is dependent on its type:
'access' \0 <BARE_JID> \0 <EXPIRES_AT> \0 <MAC>
'refresh' \0 <BARE_JID> \0 <EXPIRES_AT> \0 <SEQUENCE_NO> \0 <MAC>
'provision' \0 <BARE_JID> \0 <EXPIRES_AT> \0 <VCARD> \0 <MAC>
For example (these tokens are randomly generated, hence field values don't make much sense - line breaks are inserted only for the sake of formatting,<vCard/>
inner XML is snipped):
'access' \0 Q8@localhost \0 64875466454
\0 0acd0a66d06934791d046060cf9f1ad3c2abb3274cc7e7d7b2bc7e2ac4453ed774b6c6813b40ebec2bbc3774d59d4087
'refresh' \0 qp@localhost \0 64875466457 \0 6
\0 8f57cb019cd6dc6e7779be165b9558611baf71ee4a40d03e77b78b069f482f96c9d23b1ac1ef69f64c1a1db3d36a96ad
'provision' \0 Xmi4@localhost \0 64875466458 \0 <vCard>...</vCard>
\0 86cd344c98b345390c1961e12cd4005659b4b0b3c7ec475bde9acc9d47eec27e8ddc67003696af582747fb52e578a715
Requesting access or refresh tokens when logged in
<iq type='get' to='john@localhost' id='123'>
<query xmlns='erlang-solutions.com:xmpp:token-auth:0'/>
</iq>
To request access and refresh tokens for the first time a client should send an IQ stanza after they have successfully authenticated for the first time using some other method.
Token response format
Requested tokens are being returned by the server wrapped in IQ stanza with the following fields:
id
: value taken from the request IQ stanzatype
: resultfrom
: bare user JIDto
: full user JID
Example response (encoded tokens have been truncated in this example):
<iq id='123' type='result' from='john@localhost' to='john@localhost/res1'>
<items xmlns='erlang-solutions.com:xmpp:token-auth:0'>
<access_token>cmVmcmVzaAGQ1Mzk1MmZlYzhkYjhlOTQzM2UxMw==</access_token>
<refresh_token>cmVmcmVzaAGQ1Mzk1MmZlYzhkYjhlOTQzM2UxMw==</refresh_token>
</items>
</iq>
Once a client has obtained a token, they may start authenticating using the X-OAUTH
SASL mechanism when reaching the authentication phase of an XMPP connection initiation.
Login with access or refresh token
In order to log into the XMPP server using a previously requested token, a client should send the following stanza:
<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="X-OAUTH">
cmVmcmVzaAGQ1Mzk1MmZlYzhkYjhlOTQzM2UxMw==
</auth>
The Base64 encoded content is a token obtained prior to authentication. Authentication will succeed unless the used tokens are expired, revoked, or the keys required for MAC verification could not be found by the server.
When using a refresh token to authenticate with the server, the server will respond with a new access token:
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
cmVmcmVzaAGQ1Mzk1MmZlYzhkYjhlOTQzM2UxMw==
</success>
The above response is to be expected unless the refresh token used is expired or there were some problems processing the key on the server side.
Token revocation using command line tool
Refresh tokens issued by the server can be used to:
- log in a user: as an authentication valet,
- request a new access token with refreshed expiry date.
An administrator may revoke a refresh token:
mongooseimctl revoke_token owner@xmpphost
A client can no longer use a revoked token either for authentication or requesting new access tokens. After a client's token has been revoked, in order to obtain a new refresh token a client has to log in using some other method.
Caveat: as of now, the user's session is not terminated automatically on token revocation.
Therefore, the user might request a new set of tokens for as long as the session is active, even though their previous token was just revoked (possibly due to a breach / token leak).
Moreover, an access token still kept on a compromised device can be used to establish a new session for as long as it's valid - access tokens can't be revoked.
To alleviate rerequesting tokens by the user, an operator can use mod_admin
extension allowing to terminate the user's connection.
Access token validity can't be sidestepped right now.
Example configuration
{modules, [
{mod_auth_token, [{{validity_period, access}, {13, minutes}},
{{validity_period, refresh}, {13, days}}]
]}.