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##

  1. When the user opens an app in the DApp Store, it requests a challenge transaction from the application.
  2. 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.
  3. The application generates the challenge transaction on request, signs it with its own secret seed, and sends it to user.
  4. 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.
  5. 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.


What’s Next