Login/logout - MFA disabled
This section describes how to manage the login and logout processes for user and bot accounts registered on infrastructures where MFA are not enabled.
The following sequence diagram illustrates the access_token lifetime and the API requests allowing to manage it:
sequenceDiagram
participant Client
participant Server
participant Portal
autonumber
Client->>+Portal: Tenant lookup (email)
Portal-->>-Client: Tenant found (server URL)
Client->>+Server: Login on device (email+password)
Server-->>-Client: Authenticated (access_token+refresh_token)
loop Valid access token
Client->>+Server: Access to API endpoints (access_token)
Server-->>-Client: OK (20x resp.)
end
Note left of Server: access_token has expired
Client->>+Server: Access to an API endpoint (access_token)
Server-->>-Client: Error due to the access_token expiration (403 resp.)
Client->>+Server: Tokens renewal (refresh_token)
Server-->>-Client: Tokens renewed (access_token + refresh_token)
Client->>+Server: Logout from device
Server-->>-Client: OK (200 resp.)
Server lookup (steps 1-2)
The Citadel infrastructure is composed of several servers which are in charge to handle clients requests. The
distribution of the clients being based on their email domain, the first step consists of the server URL lookup. To
retrieve the server URL, each client shall send a GET request to the /api/get_domain endpoint (cf. Portail-API
/api/get_domain endpoint).
The following snippet retrieves the server URL for a given EMAIL:
EMAIL=pierre.deschamps@domain.org
function lookup_server() {
local hostname=$1
local email=$2
server_hostname=$(curl -s -k "https://${hostname}/api/get_domain?email=${email}" | jq -r '.domain')
echo "${server_hostname}"
}
server_hostname=$(lookup_server "join.citadel.team" "${EMAIL}")
Login (steps 3-4)
Once the server identified, the account login can be done. To archive this, a POST request shall be emit to the
/_matrix/client/r1/login endpoint (cf. TODO: Citadel-Matrix /_matrix/client/r1/login endpoint).
Once obtained, the tokens shall be stored to be able to request authenticated API endpoints and renew access_token
when it'll be outdated (cf. steps 9-10).
The following snippet logins a account using the password used during its registration:
# Never, never, never store passwords in a script... except here, for test purpose.
PASSWORD='ILoveCitadel!2023'
function login() {
local server_hostname=$1
local email=$2
local password=$3
local -n _mx_id=$4
local -n _access_token=$5
local -n _refresh_token=$6
local -n _device_id=$7
read -r -d '' data << EOM
{
"identifier": {
"address": "${email}",
"medium": "email",
"type": "m.id.thirdparty"
},
"initial_device_display_name": "Device display name",
"medium": "email",
"mfa_creds": {
"bind": true,
"id_server": "${server_hostname}",
"medium": ""
},
"password": "${password}",
"type": "m.login.password"
}
EOM
post_json "https://${server_hostname}/_matrix/client/r1/login" "${data}" "" ret http_code json
if [ ${ret} == 0 ] && [ ${http_code} == 200 ]; then
json_items=$(echo "${json}" | jq -r 'to_entries[] | @sh "[\(.key)]=\(.value)"')
declare -A items="(${json_items})"
_mx_id=${items["user_id"]}
_access_token=${items["access_token"]}
_refresh_token=${items["refresh_token"]}
_device_id=${items["device_id"]}
else
debug "Unable to login (code: ${http_code})"
debug "json=${json}"
fi
}
login "${server_hostname}" "${EMAIL}" "${PASSWORD}" mx_id access_token refresh_token device_id
The tuple "email-device" is authenticated and the received access_token can be used for HTTP requests requiring
authentication.
Success
The refresh_token shall be stored and used to renew the access_token when it will be outdated.
Warning
A device remains active until a logout.
The number of devices allowed per account being limited, it's recommanded to store and reuse the same device_id
between sessions to avoid failing authentications once the maximum number of active devices reached.
Use of access_token (steps 5-6)
Once authenticated, the access_token shall be used to request API endpoints requiring authentication.
The following snippet illustrates how to use an access_token to request an endpoint requiring authentication:
function post_json() {
local url=$1
local data=$2
local access_token=$3
local -n _ret=$4
local -n _http_code=$5
local -n _json=$6
local res=$(curl -qSs "${CURL_EXTRA_OPTIONS}" "${url}" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${access_token}" \
-d "${data}" \
-w "%{http_code}")
_ret=$?
if [ ${_ret} == 0 ]; then
_json=$(echo "${res}" | sed \$d)
fi
_http_code=$(echo "${res}" | sed \$!d)
}
function whoami() {
local server_hostname=$1
local access_token=$2
local -n _ret=$3
local -n _http_code=$4
local -n _json=$5
local url="https://${server_hostname}/_matrix/client/r0/account/whoami"
local res=$(curl -qSs "${CURL_EXTRA_OPTIONS}" "${url}" \
-H "Authorization: Bearer ${access_token}" \
-w "%{http_code}")
_ret=$?
if [ ${_ret} -eq 0 ]; then
_json=$(echo "${res}" | sed \$d)
fi
_http_code=$(echo "${res}" | sed \$!d)
}
whoami "${server_hostname}" "${_access_token}" ret whoami_http_code json
The access_token expiration will be notified by the server (cf. next section).
Access_token expiration (steps 7-8)
The access_token lifetime is managed by the backend: once the access_token expired, all requests requiring
authentication will fail (HTTP error 401 with errcode=M_EXPIRED_TOKEN returned by server).
Tokens renewal (steps 9-10)
The refresh_token allows to request new access_token and refresh_token to the server without restarting the
authentication process. To retrieve this, the client shall send a POST request to the
/_matrix/client/v2_alpha/tokenrefresh endpoint (cf. Citadel-Matrix /_matrix/client/v2_alpha/tokenrefresh endpoint).
Once obtained, the tokens shall be stored to be able to request authenticated API endpoints and renew access_token
when it'll be outdated.
The following snippet refreshes the tokens:
function refresh_token() {
local server_hostname=$1
local access_token=$2
local refresh_token=$3
local -n _ret=$4
local -n _new_access_token=$5
local -n _new_refresh_token=$6
read -r -d '' data << EOM
{
"refresh_token": "${_refresh_token}"
}
EOM
post_json "https://${server_hostname}/_matrix/client/v2_alpha/tokenrefresh" "${data}" "${_access_token}" ret http_code json
if [ ${ret} == 0 ]; then
case ${http_code} in
200)
debug "Token has been successfuly refreshed"
_new_access_token=$(echo "${json}" | jq -r '.access_token')
_new_refresh_token=$(echo "${json}" | jq -r '.refresh_token')
;;
*)
debug "Unable to refresh token (ret=${http_code})"
debug "${json}"
;;
esac
fi
}
refresh_token "${server_hostname}" "${_access_token}" "${_refresh_token}" ret new_access_token new_refresh_token
Logout (steps 11-12)
To revoke access_token and refresh_token, a logout shall be done sending a POST request to the
_matrix/client/r0/logout endpoint (cf. Matrix /_matrix/client/r0/logout endpoint)
The following snippet logout the registered account:
function logout() {
local server_hostname=$1
local -n _access_token=$2
post_json "https://${server_hostname}/_matrix/client/r0/logout" "" "${_access_token}" ret http_code json
if [ ${ret} == 0 ]; then
case ${http_code} in
200)
debug "Account successfuly logout"
exit
;;
*)
debug "Unable to logout"
debug "${json}"
;;
esac
fi
}
logout "${server_hostname}" _access_token
Example
#!/bin/bash
EMAIL='email previously used for registration'
# Never, never, never store passwords in scripts... set PASSWORD env variable instead.
CITADEL_HOSTNAME=join.thales.team
CURL_EXTRA_OPTIONS=
function debug() {
echo >&2 "$*"
}
function post_json() {
local url=$1
local data=$2
local access_token=$3
local -n _ret=$4
local -n _http_code=$5
local -n _json=$6
local res=$(curl -qSs "${CURL_EXTRA_OPTIONS}" "${url}" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${access_token}" \
-d "${data}" \
-w "%{http_code}")
_ret=$?
if [ ${_ret} == 0 ]; then
_json=$(echo "${res}" | sed \$d)
fi
_http_code=$(echo "${res}" | sed \$!d)
}
#
# Server lookup (steps 1-2)
#
function lookup_server() {
local hostname=$1
local email=$2
server_hostname=$(curl -s -k "https://${hostname}/api/get_domain?email=${email}" | jq -r '.domain')
echo "${server_hostname}"
}
#
# Login (steps 3-4)
#
function login() {
local server_hostname=$1
local email=$2
local password=$3
local -n _mx_id=$4
local -n _access_token=$5
local -n _refresh_token=$6
local -n _device_id=$7
read -r -d '' data << EOM
{
"identifier": {
"address": "${email}",
"medium": "email",
"type": "m.id.thirdparty"
},
"initial_device_display_name": "Device display name",
"medium": "email",
"mfa_creds": {
"bind": true,
"id_server": "${server_hostname}",
"medium": ""
},
"password": "${password}",
"type": "m.login.password"
}
EOM
post_json "https://${server_hostname}/_matrix/client/r1/login" "${data}" "" ret http_code json
if [ ${ret} == 0 ] && [ ${http_code} == 200 ]; then
json_items=$(echo "${json}" | jq -r 'to_entries[] | @sh "[\(.key)]=\(.value)"')
declare -A items="(${json_items})"
_mx_id=${items["user_id"]}
_access_token=${items["access_token"]}
_refresh_token=${items["refresh_token"]}
_device_id=${items["device_id"]}
else
debug "Unable to login (code: ${http_code})"
debug "json=${json}"
fi
}
#
# Use of access_token (steps 5-6)
#
# /_matrix/client/r0/account/whoami endpoint requires the use of an access token.
# _http_code will be set to 401 when an expired token is used.
#
function whoami() {
local server_hostname=$1
local access_token=$2
local -n _ret=$3
local -n _http_code=$4
local -n _json=$5
local url="https://${server_hostname}/_matrix/client/r0/account/whoami"
local res=$(curl -qSs "${CURL_EXTRA_OPTIONS}" "${url}" \
-H "Authorization: Bearer ${access_token}" \
-w "%{http_code}")
_ret=$?
if [ ${_ret} -eq 0 ]; then
_json=$(echo "${res}" | sed \$d)
fi
_http_code=$(echo "${res}" | sed \$!d)
}
#
# Access_token expiration (steps 7-8)
#
function renew_token_on_expiration() {
local server_hostname=$1
local -n _access_token=$2
local -n _refresh_token=$3
expired_token=0
begin_ts=$(date +%s)
while [ ${expired_token} == 0 ]; do
sleep 1
whoami "${server_hostname}" "${_access_token}" ret whoami_http_code json
if [ ${ret} == 0 ]; then
case ${whoami_http_code} in
200)
elapsed_sec=$(( $(date +%s) - ${begin_ts} ))
debug "Access token is valid since ${elapsed_sec}s"
;;
401)
cause=$(echo "${json}" | jq -r '.errcode')
if [ ${cause} == "M_EXPIRED_TOKEN" ]; then
refresh_token "${server_hostname}" "${_access_token}" "${_refresh_token}" ret new_access_token new_refresh_token
if [ ${ret} == 0 ]; then
begin_ts=$(date +%s)
_access_token=${new_access_token}
_refresh_token=${new_refresh_token}
fi
fi
;;
esac
fi
done
}
#
# Tokens renewal (steps 9-10)
#
function refresh_token() {
local server_hostname=$1
local access_token=$2
local refresh_token=$3
local -n _ret=$4
local -n _new_access_token=$5
local -n _new_refresh_token=$6
read -r -d '' data << EOM
{
"refresh_token": "${_refresh_token}"
}
EOM
post_json "https://${server_hostname}/_matrix/client/v2_alpha/tokenrefresh" "${data}" "${_access_token}" ret http_code json
if [ ${ret} == 0 ]; then
case ${http_code} in
200)
debug "Token has been successfuly refreshed"
_new_access_token=$(echo "${json}" | jq -r '.access_token')
_new_refresh_token=$(echo "${json}" | jq -r '.refresh_token')
;;
*)
debug "Unable to refresh token (ret=${http_code})"
debug "${json}"
;;
esac
fi
}
#
# Logout (steps 11-12)
#
function logout() {
local server_hostname=$1
local -n _access_token=$2
post_json "https://${server_hostname}/_matrix/client/r0/logout" "" "${_access_token}" ret http_code json
if [ ${ret} == 0 ]; then
case ${http_code} in
200)
debug "Account successfuly logout"
exit
;;
*)
debug "Unable to logout"
debug "${json}"
;;
esac
fi
}
#
# Login... access token expiration... its renewal... logout on ctrl+c
#
server_hostname=$(lookup_server "${CITADEL_HOSTNAME}" "${EMAIL}")
login "${server_hostname}" "${EMAIL}" "${PASSWORD}" mx_id access_token refresh_token device_id
echo "mx_id=${mx_id}"
# Never share your tokens... never, never, Never.
echo "access_token=${access_token}"
echo "refresh_token=${refresh_token}"
echo "device_id=${device_id}"
trap "logout ${server_hostname} access_token" SIGINT
renew_token_on_expiration "${server_hostname}" access_token refresh_token
To run the previous script, copy and paste it in a local file (i.e.: login-logout.sh) and run it:
Note
This script requires a bash version >=4.3 due to the use of nameref.