Authentication
Overview
When a user opens an app through the DApp Store, it tells the app what Mobius
user account it should use for payment.
The application needs to ensure that the user actually owns the secret seed to
the Mobius account and that this isn't a replay attack from a user who captured
a previous request and is replaying it.
Authentication Process##
- When the user opens an app in the DApp Store, it requests a challenge transaction from the application.
- The challenge transaction is a payment of 1 XLM from and to the application account. It is never sent to the network - it is just used for authentication.
- The application generates the challenge transaction on request, signs it with its own secret seed, and sends it to user.
- The user receives the challenge transaction and verifies it is signed by the application's secret seed by checking it against the application's published public key (that it receives through the DApp Store). Then the user signs the transaction with its own secret seed and sends it back to application along with its public key.
- The application checks that challenge transaction is now signed by itself and the public key that was passed in. Time bounds are also checked to make sure this isn't a replay attack. If everything passes, the server replies with a token the application can pass to the user to "login" with the specified public key and use it for payment (it would have previously given the app access to the public key by adding the app's public key as a signer).
Walkthrough
Setting Auth Endpoints##
The DApp store sends a GET then a POST request to an auth endpoint on your server generating a token. The user is then redirected with the token from the DApp store to your DApp.
Using Stellar Testnet or Public Network###
Stellar needs to know what development environment the project is running in. For development environments, we need to declare that we are using Stellar Test Network. For production, we need to declare the Stellar Public Network.
// Mobius is a class that needs a new instance to work with.
const mobius = new Mobius.Client();
// For development environments
mobius.network = StellarSdk.Networks.TESTNET;
// For production environments
mobius.network = StellarSdk.Networks.PUBLIC;
# For development environments, mobius-client sets :test by default
Mobius::Client.network = :test
# For production environments
Mobius::Client.network = :public
# Set testnet default, if blank is testnet
Client(network='TESTNET')
# For production environments
Client(network='PUBLIC')
// In config.php file
define('STELLAR_PUBLICNET', false);
App Key Pairs
The app key pairs to be used for test-net in development environments can be found in the dev-wallet.html file created in the Generating Key Pairs section.
Examples###
Below are complete examples of the Auth process.
const express = require('express');
const StellarSdk = require("stellar-sdk");
const Mobius = require("@mobius-network/mobius-client-js/lib");
const jwt = require('jsonwebtoken');
const app = express();
const mobius = new Mobius.Client();
mobius.network = StellarSdk.Networks.TESTNET;
app.use(express.urlencoded({ extended: true }));
// Enable CORS Access-Control-Allow-Origin all
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
// GET endpoint
// Generates and returns challenge transaction XDR signed by application to user
app.get("/", (req, res) => {
// Using Environment Variables is highly suggested for security
// Replace with your App Secret Seed
const APP_SECRET_SEED = 'SD3XEDAPTGELU2L6XMX....KZEZRGJSPQNTXZRIC';
res.send(Mobius.Auth.Challenge.call(APP_SECRET_SEED));
});
// POST endpoint example
// Validates challenge transaction
app.post("/", (req, res) => {
// Using Environment Variables is highly suggested for security
// Replace with your App Secret Seed
const APP_SECRET_SEED = 'SD3XEDAPTGELU2L6XMX....KZEZRGJSPQNTXZRIC';
const APP_DOMAIN = 'localhost:3000';
try {
const token = new Mobius.Auth.Token(
APP_SECRET_SEED,
req.body.xdr || req.query.xdr,
req.body.public_key || req.query.public_key
);
token.validate();
const payload = {
sub: token._address,
jti: token.hash("hex").toString(),
iss: 'https://' + APP_DOMAIN + '/',
iat: parseInt(token.timeBounds.minTime, 10),
exp: parseInt(token.timeBounds.maxTime, 10),
};
res.send(jwt.sign(payload, APP_SECRET_SEED));
} catch (error) {
res.status(401).json({ error: error.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Listening on port ${PORT}...`));
class AuthController < ApplicationController
skip_before_action :verify_authenticity_token, :only => [:authenticate]
# GET /auth
# Generates and returns challenge transaction XDR signed by application to user
def challenge
render plain: Mobius::Client::Auth::Challenge.call(
Rails.application.secrets.app[:secret_seed], # SA2VTRSZPZ5FIC.....I4QD7LBWUUIK
12.hours # Session duration
)
end
# POST /auth
# Validates challenge transaction. It must be:
# - Signed by application and requesting user.
# - Not older than 10 seconds from now (see Mobius::Client.strict_interval`)
def authenticate
token = Mobius::Client::Auth::Token.new(
Rails.application.secrets.app[:secret_seed], # SA2VTRSZPZ5FIC.....I4QD7LBWUUIK
params[:xdr], # Challenge transaction
params[:public_key] # User's public key
)
# Important! Otherwise, token will be considered valid.
token.validate!
# Converts issued token into JWT and sends it to user.
#
# Note: this is not the requirement. Instead of JWT, application might save token.hash along
# with time frame and public key to local database and validate over it.
render plain: Mobius::Client::Auth::Jwt.new(
Rails.application.secrets.app[:jwt_secret]
).encode(token)
rescue Mobius::Client::Error::Unauthorized
# Signatures are invalid
render plain: "Access denied!"
rescue Mobius::Client::Error::TokenExpired
# Current time is outside session time bounds
render plain: "Session expired!"
rescue Mobius::Client::Error::TokenTooOld
# Challenge transaction was issued more than 10 seconds ago
render plain: "Challenge tx expired!"
end
end
import os
import json
import binascii
import datetime
from flask import Flask
from flask_cors import CORS
from flask import jsonify, abort, request
from mobius_client_python.client import Client
from mobius_client_python.auth.challenge import Challenge
from mobius_client_python.auth.token import Token
from mobius_client_python.auth.jwt import Jwt
from stellar_base.keypair import Keypair
from flask_jwt import JWT
from werkzeug.security import safe_str_cmp
# Flask app
app = Flask(__name__)
# Set testnet
Client(network='TESTNET')
# Enable cors
CORS(app)
app.config['DEBUG'] = True
# Set DApp secret key (should use environment variables)
app.config['APP_KEY'] = SA2VTRSZPZ5FIC.....I4QD7LBWUUIK
app_key = app.config['APP_KEY']
dev_keypair = Keypair.from_seed(app_key)
@app.route('/auth', methods=['GET','POST'])
def index():
try:
if request.method == 'GET':
# set timeout (optional)
time = datetime.datetime.now() + datetime.timedelta(days=60)
# Generates challenge transaction XDR signed by application to user
challenge_te_xdr = Challenge(developer_secret=dev_keypair.seed(),
expires_in=time)\
.call()
# Return challenge transaction XDR
return challenge_te_xdr
elif request.method == 'POST':
# Validate challenge transaction and generate token
token = Token(
developer_secret=app_key,
te_xdr=request.args['xdr'],
address=request.args['public_key']
)
# Important, validate the token
token.validate()
jwt = Jwt(secret=app_key)
jwt_token = jwt.encode(token=token)
return jsonify(jwt_token.decode())
except Exception as error:
return error
<?php
include_once 'config.php';
require __DIR__ . '/vendor/autoload.php';
$url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$endpoint = parse_url($url, PHP_URL_PATH);
header("Access-Control-Allow-Origin: *");
header('Content-type:application/json;charset=utf-8');
if($endpoint == '/auth') {
if($_SERVER['REQUEST_METHOD'] == 'GET'){
try{
$expire_in = 86400;
$challenge = Mobius\Client\Auth\Challenge::generate_challenge(APP_KEY, $expire_in);
echo $challenge;
}catch(\Exception $e){
echo $e->getMessage();
}
die;
}
else if($_SERVER['REQUEST_METHOD'] == 'POST') {
try{
$xdr = base64_decode($_REQUEST['xdr']);
$public_key = $_REQUEST['public_key'];
$token = new Mobius\Client\Auth\Token(APP_KEY, $xdr, $public_key);
$token->validate();
$jwt_token = new Mobius\Client\Auth\JWT(APP_KEY);
echo $jwt_token->encode($token);
}catch(\Exception $e){
echo $e->getMessage();
}
die;
}
die;
}
HTTPS Required on Production Servers
Your production server must use HTTPS and set the below header on the auth endpoint:
Access-Control-Allow-Origin: *
Using Environment Variables
Make sure you keep all the secret seeds stored safely, extracting into ENV variables or equivalent. Keys present throughout the documentation only serve for example purposes and belong to an account in the Test network.
Testing Auth Endpoints##
Authentication can be tested using the dev-wallet.html file generated in the previous Generating Keypairs section.
Open the dev-wallet.html file and set the auth endpoint to your local server and the redirect endpoint to your DApp.
Open any of the accounts to test authentication, a successful authentication attempt will redirect from the wallet to the redirect endpoint set along with a JWT token hash as a url parameter.
See Individual DApp Store SDK Auth Methods
Challenge - Generates challenge transaction on developer's side.
Jwt - Generates JWT based on valid transaction.
Sign - Signs challenge transaction on user's side (client-side).
Token - Checks challenge transaction signed by user on DApp's side.
Updated almost 7 years ago