Skip to content

Commit

Permalink
Restore and refresh tokens (#55)
Browse files Browse the repository at this point in the history
* Restore and refresh tokens

* Added refresh_token to session
* Added option to store and restore a session

* Detect if refresh token has expired

* Review fixes

* Review fixes

* Added global and per-request retries of failed requests (#56)

* Simplified API calls

* Added support for retries

* Update README.md

* Review fix

* Added cancellation tokens

* Update CHANGELOG.md

* Update CHANGELOG.md
  • Loading branch information
britzl authored Sep 1, 2022
1 parent d341e59 commit f4d552d
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 35 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]
### Added
- Added utility functions to store and restore tokens
- Added a refresh token to the session table and functions to detect expired or soon to be expired tokens
- Added global and per-request retries of failed requests
- Added cancellation token for Rest API requests

Expand Down
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,34 +69,43 @@ local client = nakama.create_client(config)

local session = client.authenticate_email(email, password)

print(session.created)
print(session.token) -- raw JWT token
print(session.expires)
print(session.user_id)
print(session.username)
print(session.expires)
print(session.created)
print(session.refresh_token) -- raw JWT token for use when refreshing the session
print(session.refresh_token_expires)
print(session.refresh_token_user_id)
print(session.refresh_token_username)

-- Use the token to authenticate future API requests
nakama.set_bearer_token(client, session.token)

-- Use the refresh token to refresh the authentication token
nakama.session_refresh(client, session.refresh_token)
```

It is recommended to store the auth token from the session and check at startup if it has expired. If the token has expired you must reauthenticate. The expiry time of the token can be changed as a setting in the server.
It is recommended to store the auth token from the session and check at startup if it has expired. If the token has expired you must reauthenticate. If the token is about to expire it has to be refreshed. The expiry time of the token can be changed as a setting in the server. You can store the session using `session.store(session)` and later restored it using `session.restore()`:

```lua
local nakama_session = require "nakama.session"

local client = nakama.create_client(config)

-- Assume we've stored the auth token
local token = sys.load(token_path)

-- Note: creating session requires a session table, or at least a table with 'token' key
local session = nakama_session.create({ token = token })
if nakama_session.expired(session) then
print("Session has expired. Must reauthenticate.")
-- authenticate and store the auth token
else
client.set_bearer_token(session.token)
-- restore a session
local session = nakama_session.restore()

if session and nakama_session.is_token_expired_soon(session) and not nakama.is_refresh_token_expired(session) then
print("Session has expired or is about to expire. Refreshing.")
session = nakama.session_refresh(client, session.refresh_token)
nakama_session.store(session)
elseif not session or nakama_session.is_refresh_token_expired(session) then
print("Session does not exist or it has expired. Must reauthenticate.")
session = client.authenticate_email("bjorn@defold.se", "foobar123", nil, true, "britzl")
nakama_session.store(session)
end
client.set_bearer_token(session.token)
```

### Requests
Expand Down
41 changes: 37 additions & 4 deletions example/example.script
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ local nakama = require "nakama.nakama"
local log = require "nakama.util.log"
local retries = require "nakama.util.retries"
local defold = require "nakama.engine.defold"
local nakama_session = require "nakama.session"


local function email_login(client, email, password, username)
local result = client.authenticate_email(email, password, nil, true, username)
if result.token then
client.set_bearer_token(result.token)
local session = client.authenticate_email(email, password, nil, true, username)
if session.token then
nakama_session.store(session)
client.set_bearer_token(session.token)
return true
end
log("Unable to login")
Expand All @@ -30,6 +32,35 @@ local function device_login(client)
return false
end

local function refresh_session(client, session)
session = nakama.session_refresh(client, session.refresh_token)
if session.token then
nakama_session.store(session)
client.set_bearer_token(session.token)
return true
end
log("Unable to refresh session")
return false
end

local function login(client)
-- restore a session
local session = nakama_session.restore()

local success = true

if session and nakama_session.is_token_expired_soon(session) and not nakama.is_refresh_token_expired(session) then
log("Session has expired or is about to expire. Refreshing.")
success = refresh_session(client, session)
elseif not session or nakama_session.is_refresh_token_expired(session) then
log("Session does not exist or it has expired. Must reauthenticate.")
success = email_login(client, "bjorn@defold.se", "foobar123", "britzl")
else
client.set_bearer_token(session.token)
end
return success
end

function init(self)
log.print()

Expand All @@ -44,10 +75,12 @@ function init(self)
local client = nakama.create_client(config)

nakama.sync(function()
local ok = email_login(client, "bjorn@defold.se", "foobar123", "britzl")

local ok = login(client)
if not ok then
return
end

local account = client.get_account()
pprint(account)

Expand Down
99 changes: 81 additions & 18 deletions nakama/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,98 @@ local M = {}
local JWT_TOKEN = "^(.-)%.(.-)%.(.-)$"


--- Check whether a Nakama session has expired or not.
-- @param session The session object created with session.create.
-- @return A boolean if the session has expired or not.
function M.expired(session)
--- Check whether a Nakama session token is about to expire (within 24 hours)
-- @param session The session object created with session.create().
-- @return A boolean if the token is about to expire or not.
function M.is_token_expired_soon(session)
assert(session and session.expires, "You must provide a session")
return os.time() + (60 * 60 * 24) > session.expires
end

--- Check whether a Nakama session token has expired or not.
-- @param session The session object created with session.create().
-- @return A boolean if the token has expired or not.
function M.is_token_expired(session)
assert(session and session.expires, "You must provide a session")
return os.time() > session.expires
end
-- for backwards compatibility
function M.expired(session)
return M.is_token_expired(session)
end

--- Check whether a Nakama session refresh token has expired or not.
-- @param session The session object created with session.create().
-- @return A boolean if the refresh token has expired or not.
function M.is_refresh_token_expired(session)
assert(session, "You must provide a session")
if not session.refresh_token_expires then
return true
end
return os.time() > session.refresh_token_expires
end

--- Decode JWT token
-- @param token base 64 encoded JWT token
-- @return decoded token table
local function decode_token(token)
local p1, p2, p3 = token:match(JWT_TOKEN)
assert(p1 and p2 and p3, "jwt is not valid")
return json.decode(b64.decode(p2))
end

--- Create a session object with the given data and included token.
-- @param data A data table containing a "token" attribute.
-- @param data A data table containing a "token", "refresh_token" and other additional information.
-- @return The session object.
function M.create(data)
local token = data.token
assert(token, "You must provide a token")

local p1, p2, p3 = token:match(JWT_TOKEN)
assert(p1 and p2 and p3, "jwt is not valid")
assert(data.token, "You must provide a token")

log(p2)
local decoded = json.decode(b64.decode(p2))
local session = {
token = token,
created = os.time(),
expires = decoded.exp,
username = decoded.usn,
user_id = decoded.uid,
vars = decoded.vrs
created = os.time()
}

local decoded_token = decode_token(data.token)
session.token = data.token
session.expires = decoded_token.exp
session.username = decoded_token.usn
session.user_id = decoded_token.uid
session.vars = decoded_token.vrs

if data.refresh_token then
local decoded_refresh_token = decode_token(data.refresh_token)
session.refresh_token = data.refresh_token
session.refresh_token_expires = decoded_refresh_token.exp
session.refresh_token_username = decoded_refresh_token.usn
session.refresh_token_user_id = decoded_refresh_token.uid
session.refresh_token_vars = decoded_refresh_token.vrs
end
return session
end


local function get_session_save_filename()
local project_tite = sys.get_config("project.title")
local application_id = b64.encode(project_tite)
return sys.get_save_file(application_id, "nakama.session")
end

--- Store a session on disk
-- @param session The session to store
-- @return sucess
function M.store(session)
assert(session)
local filename = get_session_save_filename()
return sys.save(filename, session)
end

--- Restore a session previously stored using session.store()
-- @return The session or nil if no session has been stored
function M.restore()
local filename = get_session_save_filename()
local session = sys.load(filename)
if not session.token then
return nil
end
return session
end

Expand Down

0 comments on commit f4d552d

Please sign in to comment.