Sign in with Twitter

to your Crystal web app

adding

Sign in with Twitter

to your Crystal web app

adding

  • What and why
  • Setting up your Twitter app
  • Integration with a Crystal back end
  • Integration with a Vue.js front end
  • References

What we'll build

Sign in

Authorize

Logout

Why do I want this?

  • Friction-less sign-up process
  • Brand association
  • User management: outsourced
  • No forgotten passwords

Why do I want this?

  • Friction-less sign-up process
  • Brand association
  • User management: outsourced
  • No forgotten passwords

What's the catch?

Session management is still on you

Who does this?

gitter.im

medium.com

dev.to

Setting up your Twitter app

click

Check!

App settings

Check!

App settings

Where are my keys?

Show me the code

3-legged OAuth

in a nutshell

  • Step 1: our app initiates the authentication flow
  • Step 2: the user authorises our app to access their Twitter information
  • Step 3: our app is granted permission by Twitter to act on behalf of the user

3-legged OAuth

Step 1

3-legged OAuth

Step 2

3-legged OAuth

Step 3

3-legged OAuth

3-legged OAuth

made easy with

twitter_auth

  • Step 1: get a request token with TwitterAPI#get_token
  • Step 2: redirect to Twitter authorise page TwitterAPI.authenticate_url
  • Step 3: upgrade a request token to an access one with TwitterAPI#upgrade_token

Setup

require "twitter_auth"
require "kemal"
require "uuid"

consumer_key    = ENV["TWITTER_CONSUMER_KEY"]
consumer_secret = ENV["TWITTER_CONSUMER_SECRET"]
callback_url    = ENV["TWITTER_CALLBACK_URL"]
callback_path   = URI.parse(callback_url).path

auth_client = TwitterAPI.new(
  consumer_key, consumer_secret, callback_url)

Users = Hash(String, TwitterAPI::TokenPair).new
Tokens = Set(String).new

Setup

require "twitter_auth"
require "kemal"
require "uuid"

consumer_key    = ENV["TWITTER_CONSUMER_KEY"]
consumer_secret = ENV["TWITTER_CONSUMER_SECRET"]
callback_url    = ENV["TWITTER_CALLBACK_URL"]
callback_path   = URI.parse(callback_url).path

auth_client = TwitterAPI.new(
  consumer_key, consumer_secret, callback_url)

Users = Hash(String, TwitterAPI::TokenPair).new
Tokens = Set(String).new

Setup

Auxiliary data structures for session management

require "twitter_auth"
require "kemal"
require "uuid"

consumer_key    = ENV["TWITTER_CONSUMER_KEY"]
consumer_secret = ENV["TWITTER_CONSUMER_SECRET"]
callback_url    = ENV["TWITTER_CALLBACK_URL"]
callback_path   = URI.parse(callback_url).path

auth_client = TwitterAPI.new(
  consumer_key, consumer_secret, callback_url)

Users = Hash(String, TwitterAPI::TokenPair).new
Tokens = Set(String).new
get "/authenticate" do |ctx|
  request_token = auth_client.get_token.oauth_token

  Tokens.add request_token
  ctx.redirect TwitterAPI.authenticate_url(request_token)
end

click

get "/authenticate" do |ctx|
  request_token = auth_client.get_token.oauth_token

  Tokens.add request_token
  ctx.redirect TwitterAPI.authenticate_url(request_token)
end

Obtain a request token

Step 1

get "/authenticate" do |ctx|
  request_token = auth_client.get_token.oauth_token

  Tokens.add request_token
  ctx.redirect TwitterAPI.authenticate_url(request_token)
end
get "/authenticate" do |ctx|
  request_token = auth_client.get_token.oauth_token

  Tokens.add request_token
  ctx.redirect TwitterAPI.authenticate_url(request_token)
end

Redirect to Twitter authorise page

Step 2

get callback_path do |ctx|
  token = ctx.params.query["oauth_token"]

  halt(ctx, status_code: 400) unless Tokens.includes? token
  Tokens.delete(token)

  verifier = ctx.params.query["oauth_verifier"]
  token, secret = auth_client.upgrade_token(token, verifier)
  
  app_token = UUID.random.to_s
  Users[app_token] = TwitterAPI::TokenPair.new(token, secret)

  ctx.response.headers.add "Location", "/?token=#{app_token}"
  ctx.response.status_code = 302
end

click

my-usr

*******

get callback_path do |ctx|
  token = ctx.params.query["oauth_token"]

  halt(ctx, status_code: 400) unless Tokens.includes? token
  Tokens.delete(token)

  verifier = ctx.params.query["oauth_verifier"]
  token, secret = auth_client.upgrade_token(token, verifier)
  
  app_token = UUID.random.to_s
  Users[app_token] = TwitterAPI::TokenPair.new(token, secret)

  ctx.response.headers.add "Location", "/?token=#{app_token}"
  ctx.response.status_code = 302
end

my-usr

*******

get callback_path do |ctx|
  token = ctx.params.query["oauth_token"]

  halt(ctx, status_code: 400) unless Tokens.includes? token
  Tokens.delete(token)

  verifier = ctx.params.query["oauth_verifier"]
  token, secret = auth_client.upgrade_token(token, verifier)
  
  app_token = UUID.random.to_s
  Users[app_token] = TwitterAPI::TokenPair.new(token, secret)

  ctx.response.headers.add "Location", "/?token=#{app_token}"
  ctx.response.status_code = 302
end

my-usr

*******

get callback_path do |ctx|
  token = ctx.params.query["oauth_token"]

  halt(ctx, status_code: 400) unless Tokens.includes? token
  Tokens.delete(token)

  verifier = ctx.params.query["oauth_verifier"]
  token, secret = auth_client.upgrade_token(token, verifier)
  
  app_token = UUID.random.to_s
  Users[app_token] = TwitterAPI::TokenPair.new(token, secret)

  ctx.response.headers.add "Location", "/?token=#{app_token}"
  ctx.response.status_code = 302
end

my-usr

*******

Upgrade request token to access token

Step 3

get callback_path do |ctx|
  token = ctx.params.query["oauth_token"]

  halt(ctx, status_code: 400) unless Tokens.includes? token
  Tokens.delete(token)

  verifier = ctx.params.query["oauth_verifier"]
  token, secret = auth_client.upgrade_token(token, verifier)
  
  app_token = UUID.random.to_s
  Users[app_token] = TwitterAPI::TokenPair.new(token, secret)

  ctx.response.headers.add "Location", "/?token=#{app_token}"
  ctx.response.status_code = 302
end

my-usr

*******

get callback_path do |ctx|
  token = ctx.params.query["oauth_token"]

  halt(ctx, status_code: 400) unless Tokens.includes? token
  Tokens.delete(token)

  verifier = ctx.params.query["oauth_verifier"]
  token, secret = auth_client.upgrade_token(token, verifier)
  
  app_token = UUID.random.to_s
  Users[app_token] = TwitterAPI::TokenPair.new(token, secret)

  ctx.response.headers.add "Location", "/?token=#{app_token}"
  ctx.response.status_code = 302
end

my-usr

*******

What now?

get "/verify" do |ctx|
  _, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?

  ctx.response.content_type = "application/json"
  auth_client.verify(twitter_token)
end
get "/verify" do |ctx|
  _, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?

  ctx.response.content_type = "application/json"
  auth_client.verify(twitter_token)
end
get "/verify" do |ctx|
  _, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?

  ctx.response.content_type = "application/json"
  auth_client.verify(twitter_token)
end
get "/logout" do |ctx|
  app_token, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?
  
  auth_client.invalidate_token(twitter_token)
  Users.delete(app_token)
  
  ctx.redirect "/"
end

click

get "/logout" do |ctx|
  app_token, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?
  
  auth_client.invalidate_token(twitter_token)
  Users.delete(app_token)
  
  ctx.redirect "/"
end
get "/logout" do |ctx|
  app_token, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?
  
  auth_client.invalidate_token(twitter_token)
  Users.delete(app_token)
  
  ctx.redirect "/"
end
get "/logout" do |ctx|
  app_token, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?
  
  auth_client.invalidate_token(twitter_token)
  Users.delete(app_token)
  
  ctx.redirect "/"
end
get "/logout" do |ctx|
  app_token, twitter_token = credentials(ctx)
  halt(ctx, status_code: 401) if twitter_token.nil?
  
  auth_client.invalidate_token(twitter_token)
  Users.delete(app_token)
  
  ctx.redirect "/"
end

click

A sample front end integration with Vue.js

Front end states

html

<body>
  <div class="content" id="app">
    <div v-if="loaded">
      <div v-if="logged_in">
        You are logged in as {{ username }}
        <a href="#" v-on:click="logout">Logout</a>
      </div>
      <div v-else>
        <a href="/authenticate"><img src="sign-in.png"></a>
      </div>
    </div>
    <div v-else>Loading</div>
  </div>
  <script src="index.js"></script>
</body>
<body>
  <div class="content" id="app">
    <div v-if="loaded">
      <div v-if="logged_in">
        You are logged in as {{ username }}
        <a href="#" v-on:click="logout">Logout</a>
      </div>
      <div v-else>
        <a href="/authenticate"><img src="sign-in.png"></a>
      </div>
    </div>
    <div v-else>Loading</div>
  </div>
  <script src="index.js"></script>
</body>

html

<body>
  <div class="content" id="app">
    <div v-if="loaded">
      <div v-if="logged_in">
        You are logged in as {{ username }}
        <a href="#" v-on:click="logout">Logout</a>
      </div>
      <div v-else>
        <a href="/authenticate"><img src="sign-in.png"></a>
      </div>
    </div>
    <div v-else>Loading</div>
  </div>
  <script src="index.js"></script>
</body>

html

JS

var app = new Vue({
  el: '#app',
  data: {
    loaded: false,
    logged_in: false,
    username: null
  },
  // ...
})

JS

// page initialisation
var urlParams = new URLSearchParams(window.location.search)
token = urlParams.get("token")
if(token == null) {
  this.logged_in = false
  this.loaded = true
} else {
  fetch('/verify', {headers: {token}})
    .then(response => response.json())
    .then(data => {
      this.logged_in = true
      this.username = data.name
      this.loaded = true
    })
}

JS

// page initialisation
var urlParams = new URLSearchParams(window.location.search)
token = urlParams.get("token")
if(token == null) {
  this.logged_in = false
  this.loaded = true
} else {
  fetch('/verify', {headers: {token}})
    .then(response => response.json())
    .then(data => {
      this.logged_in = true
      this.username = data.name
      this.loaded = true
    })
}

JS

// page initialisation
var urlParams = new URLSearchParams(window.location.search)
token = urlParams.get("token")
if(token == null) {
  this.logged_in = false
  this.loaded = true
} else {
  fetch('/verify', {headers: {token}})
    .then(response => response.json())
    .then(data => {
      this.logged_in = true
      this.username = data.name
      this.loaded = true
    })
}

JS

// page initialisation
var urlParams = new URLSearchParams(window.location.search)
token = urlParams.get("token")
if(token == null) {
  this.logged_in = false
  this.loaded = true
} else {
  fetch('/verify', {headers: {token}})
    .then(response => response.json())
    .then(data => {
      this.logged_in = true
      this.username = data.name
      this.loaded = true
    })
}
<body>
  <div class="content" id="app">
    <div v-if="loaded">
      <div v-if="logged_in">
        You are logged in as {{ username }}
        <a href="#" v-on:click="logout">Logout</a>
      </div>

html

JS

  fetch('/verify', {headers: {token}})
    .then(response => response.json())
    .then(data => {
      this.logged_in = true
      this.username = data.name
      this.loaded = true
    })
}

+

=

References

Made with Slides.com