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,137