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
- All the code shown + demo on heroku
- Twitter's official guide to Log in with Twitter
- Twitter's official docs on 3-legged OAuth
- twitter_auth: a Crystal shard to simplify the 3-legged OAuth1.0a flow for Twitter
Sign in with Twitter
By Lorenzo Barasti
Sign in with Twitter
- 1,204