Sign in with Twitter
to your Crystal web app
adding
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043675/Twitter_Logo_Blue.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043841/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043843/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
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?
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043805/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043809/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043823/pasted-from-clipboard.png)
gitter.im
medium.com
dev.to
Setting up your Twitter app
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043714/pasted-from-clipboard.png)
click
Check!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043727/pasted-from-clipboard.png)
App settings
Check!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043727/pasted-from-clipboard.png)
App settings
Where are my keys?
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043717/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043725/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043929/oauth_flow.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043841/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043843/pasted-from-clipboard.png)
Step 1
3-legged OAuth
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043929/oauth_flow.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043841/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043843/pasted-from-clipboard.png)
Step 2
3-legged OAuth
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043929/oauth_flow.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043841/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043843/pasted-from-clipboard.png)
Step 3
3-legged OAuth
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043929/oauth_flow.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043841/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043843/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044184/sign-in-with-twitter.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044184/sign-in-with-twitter.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044184/sign-in-with-twitter.png)
get "/authenticate" do |ctx|
request_token = auth_client.get_token.oauth_token
Tokens.add request_token
ctx.redirect TwitterAPI.authenticate_url(request_token)
end
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044184/sign-in-with-twitter.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043846/pasted-from-clipboard.png)
my-usr
*******
What now?
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044234/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044237/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044234/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044237/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044234/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044237/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044237/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
click
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043841/pasted-from-clipboard.png)
A sample front end integration with Vue.js
Front end states
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7043979/pasted-from-clipboard.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/142582/images/7044232/pasted-from-clipboard.png)
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,184