I am a newbie Rails 4 developer and need some help adding an API to my simple application using rest-client. For simplification, I will just ask about the API's authentication system.
I have a simple app which uses the Devise gem for authentication. I would like for every user that creates an account to have a calendar for scheduling and booking purposes. to achieve this I am using an API called timekit (http://timekit.io/). Their authentication system responds the to following cURL code example:
curl -X POST \
-H 'Timekit-App: docs' \
-d '{
"email": "doc.brown#timekit.io",
"password": "DeLorean"
}' \
https://api.timekit.io/v2/auth
This will then return the following JSON:
{
"data": {
"activated": true,
"email": "doc.brown#timekit.io",
"first_name": "Dr. Emmett",
"img": "http://www.gravatar.com/avatar/7a613e5348d63476276935025",
"last_name": "Brown",
"last_sync": null,
"name": "Dr. Emmett Brown",
"timezone": "America/Los_Angeles",
"token": "UZpl3v3PTP1PRwqIrU0DSVpbJkNKl5gN",
"token_generated_at": null,
"api_token": "FluxCapacitator", // note that this is usually a randomized string and not an actual word
}
}
So now my questions are the following:
1) Where in the Rails framework do I implement this?
2) How do I do so using rest-client instead of cURL?
3) How do I integrate this with Devise?
4)What are good resources to enhance my own understanding of what I am actually doing here?
Awesome to see your using Timekit (I'm one of the core devs) :)
We don't currently have a ruby gem and I'm not a Ruby developer, but here's a quick code example on how to accomplish this with the HTTParty library:
# Global configs for "admin" user
TK_ADMIN_USER = 'my-email#gmail.com'
TK_ADMIN_TOKEN = '12345ABCD'
# Example usage:
# timekit = Timekit.new(TK_ADMIN_USER, TK_ADMIN_TOKEN)
# tk_user = timekit.create_user(account)
# someInternalUser.update(tk_token: tk_user.token)
class Timekit
include HTTParty
base_uri 'https://api.timekit.io'
headers 'Timekit-App' => 'your-app-name'
def initialize(user, password)
#auth = {username: user, password: password}
end
def create_user(account)
options = {
body: {
"email" => account.email,
"first_name" => account.first_name,
"last_name" => account.last_name,
"timezone" => "America/Los_Angeles"
}.to_json,
basic_auth: #auth,
}
self.class.post('/v2/users/', options)
end
def create_calendar(account)
options = {
body: {
name: "Bookings",
description: "Hold bookings for clients."
}.to_json,
basic_auth: #auth
}
self.class.post('/v2/calendars', options)
end
end
https://gist.github.com/laander/83cb7f5dde1f933173c7
In general, the idea is to create a user through our API (you can do it transparently whenever a user signs up in your onboarding) and then save the API Token that the API returns. After that, you can perform API calls impersonating that users (fetch calendars, create events, query availability etc)
Just write us in the chat on timekit.io if you need more hands-on help!
You might consider creating a small library to wrap interactions with the Timekit web API in the /lib directory of your project. I didn't see a gem for this web API, so you might consider extracting this logic into one so the community can benefit.
The rest-client gem appears pretty easy to use:
auth_hash = {
"email" => "doc.brown#timekit.io",
"password" => "DeLorean"
}
RestClient.post "https://api.timekit.io/v2/auth", auth_hash.to_json, content_type: :json, accept: :json
If you are creating a new Timekit account for each user, you might consider adding the credential to the User model or another related model that stores the credential.
Related
I want to send a transactional mail via Sendgrid when a user registers (I use devise for authentication). I had this working fine in my_mailer.rb using SMTP as follows:
def confirmation_instructions(record, token, opts={})
# SMTP header for Sendgrid - v2
# headers["X-SMTPAPI"]= {
# "sub": {
# "-CONFIRM_TOKEN-": [
# token
# ]
# },
# "filters": {
# "templates": {
# "settings": {
# "enable": 1,
# "template_id": "1111111-1111-1111-1111-111111111111"
# }
# }
# }
# }.to_json
However, prompted by Sendgrid to use v3 syntax to support newer mail templates, I changed code to the following (from the sendgrid help docs, as opposed to a real understanding):
def confirmation_instructions(record, token, opts={})
require 'sendgrid-ruby'
include SendGrid
sg = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
data = JSON.parse('{
"substitutions": {
"-CONFIRM_TOKEN-": [
token
],
"template_id": "1111111-1111-1111-1111-111111111111"
}')
response = sg.client.mail._("send").post(request_body: data)
puts response.status_code
puts response.body
puts response.parsed_body
puts response.headers
Now I get the error message:
'NoMethodError (undefined method `include' for #<MyMailer:0x0000000003cfa398>):'
If I comment out the 'include' line I get:
'TypeError (no implicit conversion of nil into String):' on the line: "sg = SendGrid..."
I use the Gem: sendgrid-ruby (5.3.0)
Any ideas would be appreciated - I've been trying to hit on the correct syntax by trial-and-error for a while now and finally admit I am stuck.
UPDATE#1:
The first issue was I was using the wrong API_KEY env. variable (copied from 2 different help docs): "SENDGRID_API_KEY" (in code) vs. SENDGRID_APIKEY_GENERAL (set in Heroku). Fixed.
UPDATE #2:
With the "include" line commented out I now seem to be getting a JSON parse error:
JSON::ParserError (416: unexpected token at 'token
So my 2 current issues are now:
(1) I would like 'token' to be the confirmation token variable but it is not being passed
(2) Sending the below simple (1 line) content of 'data' does not throw up an error, but the appropriate template within Sendgrid is not selected:
data = JSON.parse('{
"template_id": "1111111-1111-1111-1111-111111111111"
}')
UPDATE #3:
Here's an update on the status of my issue and exactly where I am now stuck:
This code works fine (using Sendgrid v2 which I am trying to upgrade from):
def confirmation_instructions(record, token, opts={})
#
# SMTP header for Sendgrid - v2
# This works fine
#
headers["X-SMTPAPI"]= {
"sub": {
"-CONFIRM_TOKEN-": [
token
]
},
"filters": {
"templates": {
"settings": {
"enable": 1,
"template_id": "1111111-1111-1111-1111-111111111111"
}
}
}
}.to_json
This Sendgrid v3 code does not work (the email does get sent via Sendgrid but it does not select the template within Sendgrid - it just uses whatever code is in app/views/my_mailer/confirmation_instructions.html.erb):
#
# Sendgrid API v3
# This sends an email alright but it takes content from app/views/my_mailer/confirmation_instructions.html.erb
# It DOES NOT select the template within Sendgrid
#
data = JSON.parse('{
"template_id": "1111111-1111-1111-1111-111111111111",
"personalizations": [
{
"substitutions": {
"-CONFIRM_TOKEN-": "'+token+'"
}
}
]
}')
sg = SendGrid::API.new(api_key: ENV['SENDGRID_APIKEY_GENERAL2'])
response = sg.client.mail._("send").post(request_body: data)
puts response.status_code
puts response.body
puts response.parsed_body
puts response.headers
As always, any insight appreciated.
For anyone trying to get SendGrid v3 API working with Ruby/Devise/Heroku and use SendGrid's dynamic transactional emails these tips may help you. I spent a week getting this to work and these steps (& mistakes I made) were not apparent in the various documentation:
Generating the SendGrid API key (on SendGrid website): when generating the key, the API key only appears once allowing you to copy it, from then on it is invisible. As I could not see the key later I mistakenly used the "API Key ID" in my Heroku environment variables, rather than the true API Key.
Ensure the name you give the key in Heroku (for me: "SENDGRID_APIKEY_GENERAL") matches the code you use to reference it i.e. sg = SendGrid::API.new(api_key: ENV['SENDGRID_APIKEY_GENERAL'])
For sending variables to be substituted in the template use "dynamic_template_data" and not "substitutions". This should be within the "personalizations" section (see code example below).
I found it useful to refer to the Sendgrid dynamic template ID by using an environment variable in Heroku (for me: 'SENDGRID_TRANS_EMAIL_CONFIRM_TEMPLATE_ID') as opposed to hard-coding in Ruby (just allowed me to experiment with different templates rather than changing code).
The correct syntax for using a variable in the JSON string in Ruby is e.g. "CONFIRM_TOKEN": "'+token+'" (see code example below)
Do not use other characters in the name: i.e. "CONFIRM_TOKEN" worked but "-CONFIRM_TOKEN-" did not work
In the HTML of the transactional email template on SendGrid use this syntax for the substitution: {{CONFIRM_TOKEN}}
When creating a transactional template on SendGrid you can only have a 'design' view or a 'code' view not both. You must select at the start when creating the template and cannot switch after.
In the devise confirmations_instructions action refer to the user as a record (e.g. email) as record.email
Gemfile: gem 'rails', '5.2.2' ruby '2.6.1' gem 'devise', '4.6.1' gem 'sendgrid-ruby', '6.0.0'
Here is my successful ruby code that I have in my_mailer.rb:
def confirmation_instructions(record, token, opts={})
data = JSON.parse('{
"personalizations": [
{
"to": [
{
"email": "'+record.email+'"
}
],
"subject": "Some nice subject line content",
"dynamic_template_data": {
"CONFIRM_TOKEN": "'+token+'",
"TEST_DATA": "hello"
}
}
],
"from": {
"email": "aaaa#aaaa.com"
},
"content": [
{
"type": "text/plain",
"value": "and easy to do anywhere, even with Ruby"
}
],
"template_id": "'+ENV['SENDGRID_TRANS_EMAIL_CONFIRM_TEMPLATE_ID']+'"
}')
sg = SendGrid::API.new(api_key: ENV['SENDGRID_APIKEY_GENERAL'])
response = sg.client.mail._("send").post(request_body: data)
puts response.status_code
puts response.body
puts response.headers
You cannot include a module in a method. You have to include it in your class, so outside of the methode, like
class SomethingMailer
require 'sendgrid-ruby'
include SendGrid
def confirmation_instructions(record, token, opts={})
...
end
end
For your third update problem:
You are not sending a JSON but instead you are creating a JSON, then parsing it into a hash, then sending that hash, instead of the JSON.
JSON.parse #parses a JSON into a Hash
You should do the opposite and have a hash that you transform into a JSON
Something like
data = {
template_id: your_template_id # or pass a string
personalizations: [
...
]
}
Then you call
data_json = data.to_json
response = sg.client.mail._("send").post(request_body: data_json)
However this does not explain why your template in app/views/my_mailer/confirmation_instructions.html.erb gets sent. So I think you are either calling a different mailer_method at the same time, or you are not calling your actual confirmation_instructions method at all. Try to confirm that you SendGrid Methods actually is called and see what it returns. It should have returned some kind of error before, when you were sending a hash instead of a string.
I'm working on an Angular app with a Rails API. I'm using Devise Token Auth and the 6.x branch of angular-token. I can CRUD users with no problem, and I can sign in, but when I try to access a restricted resource (one restricted through the before_action :authenticate_user! helper), I get an error regardless of whether I'm signed in or not.
Relevant controller:
class QuestionsController < ApplicationController
before_action :authenticate_user!
# GET /questions.json
def index
#questions = Question.all
# This code is never reached due to the :authenticate_user! helper,
# but when I comment it out, I get the following:
puts user_signed_in? # => false
puts current_user # => nil
render json: #questions
end
A few things:
I'm definitely signed in according to angular-token -- TokenService.UserSignedIn() returns true at the time of the 401 Unauthorized response.
I've enabled CORS.
I've exposed the following headers in the rack-cors config: ['access-token', 'expiry', 'token-type', 'uid', 'client'].
Any help would be greatly appreciated.
Example response from signin:
{
"data": {
"id": 1,
"email": "me#example.com",
"provider": "email",
"uid": "me#example.com",
"allow_password_change": false,
"name": null,
"nickname": null,
"image": null
}
}
Angular code:
this.authService.loginUser({
login: '',
password: ''
})
.subscribe(resp => {
if (resp.status === 200) {
this.router.navigate(['/']);
}
},
err => {
// cut for brevity
});
Edit:
I've figured out the problem is with angular-token not setting the auth headers due to what I believe to be a bug in the following code:
// Add the headers if the request is going to the configured server
if (this.tokenService.currentAuthData && req.url.match(this.tokenService.apiPath)) {
(<any>Object).assign(baseHeaders, {
'access-token': this.tokenService.currentAuthData.accessToken,
'client': this.tokenService.currentAuthData.client,
'expiry': this.tokenService.currentAuthData.expiry,
'token-type': this.tokenService.currentAuthData.tokenType,
'uid': this.tokenService.currentAuthData.uid
});
}
Since I don't have apiPath defined, req.url.match(this.tokenService.apiPath)) is evaluated to false and thus, the headers are never added.
I don't know about angular-token, but I had the same issue with my React/Redux based app - turns out that it was indeed simply a problem with the headers missing or not being properly set.
For some reason it seems like DeviseTokenAuth:: based controllers can only work by setting client and access-token, but for all other resources protected by DeviseTokenAuth::Concerns::SetUserByToken, you have to set all headers exactly as expected and documented by devise_token_auth.
Check headers values and formats.
Check that headers are exactly equal to the one returned by the last request (the authentication request in your case).
Also, check your mapping
In config/initializers/devise_token_auth.rb :
config.headers_names = {:'access-token' => 'access-token',
:'client' => 'client',
:'expiry' => 'expiry',
:'uid' => 'id',
:'token-type' => 'token-type' }
(it defines the name of the expected http headers (not the models/activerecords attributes)).
I have a firebase project which Im trying to authenticate from my rails server creating a custom token with the library ruby-jwt as it says on the docs, but i keep getting the same error:
auth/invalid-custom-token, The custom token format is incorrect. Please check the documentation.
The credentials.json is from the service account I made in google console, uid is sent from the front end to the api.
def generate_auth_token(uid)
now_seconds = Time.now.to_i
credentials = JSON.parse(File.read("credentials.json"))
private_key = OpenSSL::PKey::RSA.new credentials["private_key"]
payload = {
:iss => credentials["client_email"],
:sub => credentials["client_email"],
:aud => 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit',
:iat => now_seconds,
:exp => now_seconds+(60*60), # Maximum expiration time is one hour
:uid => uid.to_s,
:claims => {:premium_account => true}
}
JWT.encode(payload, private_key, 'RS256')
end
it looks like this in jwt.io
{
"iss": "defered#defered.iam.gserviceaccount.com",
"sub": "defered#defered.iam.gserviceaccount.com",
"aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
"iat": 1486824545,
"exp": 1486828145,
"uid": "4",
"claims": {
"premium_account": true
}
}
It looks like the accepted answer found a way to link authentication from Firebase to Rails, but the original question seems to be asking how to link Rails authentication to Firebase (which is what I was trying to do).
To keep your authentication logic in Rails (ex: from Devise) and share it with Firebase, first get a Firebase server key as a .json file from your Service Accounts page in your project's settings.
You'll only need the private_key and client_id from this file, which I recommend storing as environment variables so they're not potentially leaked in source code.
Next, make a Plain ol' Ruby object (PORO) that will take in a User and spit out a JSON Web Token (JWT) that Firebase can understand:
class FirebaseToken
def self.create_from_user(user)
service_account_email = ENV["FIREBASE_CLIENT_EMAIL"]
private_key = OpenSSL::PKey::RSA.new ENV["FIREBASE_PRIVATE_KEY"]
claims = {
isCool: "oh yeah"
}
now_seconds = Time.now.to_i
payload = {
iss: service_account_email,
sub: service_account_email,
aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
iat: now_seconds,
exp: now_seconds + (60*60), # Maximum expiration time is one hour
uid: user.id,
# a hash to pass to the client as JSON
claims: claims
}
JWT.encode payload, private_key, "RS256"
end
end
Now send this JWT to authenticated users through javascript in your application layout:
window.firebaseJWT = "#{FirebaseToken.create_from_user(current_user)}";
In your frontend code, you can now use this token to authenticate users:
firebase
.auth()
.signInWithCustomToken(window.firebaseJWT)
.catch(error => {
console.error(error);
});
Remember to sign them out of firebase when they sign out of your application:
firebase
.auth()
.signOut()
.then(() => {
// Sign-out successful.
})
.catch(error => {
console.error(error);
});
I found a better way to authenticate, I'm just sending the token that firebase gives you and verifying it on rails with the information I need and that's it.
Check if your secret key is wrapped in double quotes and not single as they contain '\n' escape sequences. An auth/invalid-custom-token error is thrown if the secret key is not as specified in the documentation.
I am building a rails 5 app that is deployed on heroku.
I want to use AWS congnito to achieve single sign on, but there are not enough example to implement it.
I am using devise for authentication. Now my goal is to put my all users on AWS cognito and authenticate them from my rails App.
This is the only resource i found on AWS congnito with rails, I am looking for some example application or a link to tools or ruby API document to achieve this.
Please Help.
Update On basis Of Bala Answer
require 'aws-sdk'
ENV['AWS_ACCESS_KEY_ID'] = 'XXXXXXXXXXXXXXXXX'
ENV['AWS_SECRET_ACCESS_KEY'] = 'XXXX+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
region_name = 'us-east-1'
endpoint = 'cognito-idp.us-east-1.amazonaws.com'
client = Aws::CognitoIdentityProvider::Client.new(
region: region_name
)
resp = client.admin_create_user({
user_pool_id: "us-east-1_iD7xNHj0x", # required
username: "Test", # required
user_attributes: [
{
name: "email", # required
value: "sachin.singh#example.com",
},
],
validation_data: [
{
name: "Email", # required
value: "AttributeValueType",
},
],
temporary_password: "PasswordType",
force_alias_creation: false,
message_action: "RESEND", # accepts RESEND, SUPPRESS
desired_delivery_mediums: ["EMAIL"], # accepts SMS, EMAIL
})
Error stack trace
home/sachin/.rvm/gems/ruby-2.1.5#global/gems/aws-sdk-core-2.6.38/lib/seahorse/client/plugins/raise_response_errors.rb:15:in `call': User does not exist. (Aws::CognitoIdentityProvider::Errors::UserNotFoundException)
from /home/sachin/.rvm/gems/ruby-2.1.5#global/gems/aws-sdk-core-2.6.38/lib/aws-sdk-core/plugins/idempotency_token.rb:18:in `call'
from /home/sachin/.rvm/gems/ruby-2.1.5#global/gems/aws-sdk-core-2.6.38/lib/aws-sdk-core/plugins/param_converter.rb:20:in `call'
from /home/sachin/.rvm/gems/ruby-2.1.5#global/gems/aws-sdk-core-2.6.38/lib/seahorse/client/plugins/response_target.rb:21:in `call'
from /home/sachin/.rvm/gems/ruby-2.1.5#global/gems/aws-sdk-core-2.6.38/lib/seahorse/client/request.rb:70:in `send_request'
from /home/sachin/.rvm/gems/ruby-2.1.5#global/gems/aws-sdk-core-2.6.38/lib/seahorse/client/base.rb:207:in `block (2 levels) in define_operation_methods'
from aws_cognito.rb:20:in `<main>'
Update 2
resp = client.admin_initiate_auth({
user_pool_id: "us-east-1_uKM", # required
client_id: "3g766413826eul9kre28qne4f", # required
auth_flow: "ADMIN_NO_SRP_AUTH",
auth_parameters: {
"EMAIL" => "kapil.sachdev#metacube.com",
"PASSWORD" => "Ibms#1234"
}
})
First of all, you need to create a user pool for your application
Use this link to create user pool through AWS console
You can find the ruby methods for sign_up, sign_in, change password and many other functions at http://docs.aws.amazon.com/sdkforruby/api/Aws/CognitoIdentityProvider/Client.html
EDIT
Now, you can sign up the user using sign_up
sign_in a user using
admin_initiate_auth
if you need mobile number confirmation, email confirmation you need to configure the user pool that you are creating.
You can find the corresponding methods for confirming the mobile numbers using http://docs.aws.amazon.com/sdkforruby/api/Aws/CognitoIdentityProvider/Client.html#confirm_sign_up-instance_method
I have two applications. One contains an API (Rails) and the second (Sinatra) is a test application to check if the API is working correctly.
In the test application I can get all of the information provided by my API using HTTParty. However, when I try to add http basic authentication in the API application,
the test application always gets a 401 error.
In the controller which provides the API I have written
http_basic_authenticate_with name: "admin", password: "admin"
In my test application I have written
authenticate = {name: "admin", password: "admin"}
options = {query: {username: params[:username], email: params[:email]},
basic_auth: authenticate}
response = HTTParty.get('http://localhost:3000/search.json', options)
What am I doing wrong?
Your key for the username is incorrect:
authenticate = {:username => 'admin', :password => 'admin'}
This should work