I need to authenticate and authorize the application using Google's Service Account flow in perl.
Google does not seem to list perl as a supported language in their documentation.
Has any one faced this issue? Pointers to any code out there?
Search for perl Google OAUTH and you'll find many different approaches.
For an example of a quick web server that can be used to collect OAUTH tokens for your suitably configured Google Cloud API Project see this:
#!perl
use strict; use warnings; ## required because I can't work out how to get percritic to use my modern config
package goauth;
# ABSTRACT: CLI tool with mini http server for negotiating Google OAuth2 Authorisation access tokens that allow offline access to Google API Services on behalf of the user.
#
# Supports multiple users
# similar to that installed as part of the WebService::Google module
# probably originally based on https://gist.github.com/throughnothing/3726907
# OAuth2 for Google. You can find the key (CLIENT ID) and secret (CLIENT SECRET) from the app console here under "APIs & Auth"
# and "Credentials" in the menu at https://console.developers.google.com/project.
# See also https://developers.google.com/+/quickstart/.
use strict;
use warnings;
use Carp;
use Mojolicious::Lite;
use Data::Dumper;
use Config::JSON;
use Tie::File;
use feature 'say';
use Net::EmptyPort qw(empty_port);
use Crypt::JWT qw(decode_jwt);
my $filename;
if ( $ARGV[0] )
{
$filename = $ARGV[0];
}
else
{
$filename = './gapi.json';
}
if ( -e $filename )
{
say "File $filename exists";
input_if_not_exists( ['gapi/client_id', 'gapi/client_secret', 'gapi/scopes'] ); ## this potentially allows mreging with a json file with data external
## to the app or to augment missing scope from file generated from
## earlier versions of goauth from other libs
runserver();
}
else
{
say "JSON file '$filename' with OAUTH App Secrets and user tokens not found. Creating new file...";
setup();
runserver();
}
sub setup
{
## TODO: consider allowing the gapi.json to be either seeded or to extend the credentials.json provided by Google
my $oauth = {};
say "Obtain project app client_id and client_secret from http://console.developers.google.com/";
print "client_id: ";
$oauth->{ client_id } = _stdin() || croak( 'client_id is required and has no default' );
print "client_secret: ";
$oauth->{ client_secret } = _stdin() || croak( 'client secret is required and has no default' );
print 'scopes ( space sep list): eg - email profile https://www.googleapis.com/auth/plus.profile.emails.read '
. "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/contacts.readonly https://mail.google.com\n";
$oauth->{ scopes } = _stdin(); ## no croak because empty string is allowed an will evoke defaults
## set default scope if empty string provided
if ( $oauth->{ scopes } eq '' )
{
$oauth->{ scopes }
= 'email profile https://www.googleapis.com/auth/plus.profile.emails.read '
. 'https://www.googleapis.com/auth/calendar '
. 'https://www.googleapis.com/auth/contacts.readonly https://mail.google.com';
}
my $tokensfile = Config::JSON->create( $filename );
$tokensfile->set( 'gapi/client_id', $oauth->{ client_id } );
$tokensfile->set( 'gapi/client_secret', $oauth->{ client_secret } );
$tokensfile->set( 'gapi/scopes', $oauth->{ scopes } );
say 'OAuth details updated!';
# Remove comment for Mojolicious::Plugin::JSONConfig compatibility
tie my #array, 'Tie::File', $filename or croak $!;
shift #array;
untie #array;
return 1;
}
sub input_if_not_exists
{
my $fields = shift;
my $config = Config::JSON->new( $filename );
for my $i ( #$fields )
{
if ( !defined $config->get( $i ) )
{
print "$i: ";
#chomp( my $val = <STDIN> );
my $val = _stdin();
$config->set( $i, $val );
}
}
return 1;
}
sub runserver
{
my $port = empty_port( 3000 );
say "Starting web server. Before authorization don't forget to allow redirect_uri to http://127.0.0.1 in your Google Console Project";
$ENV{ 'GOAUTH_TOKENSFILE' } = $filename;
my $config = Config::JSON->new( $ENV{ 'GOAUTH_TOKENSFILE' } );
# authorize_url and token_url can be retrieved from OAuth discovery document
# https://github.com/marcusramberg/Mojolicious-Plugin-OAuth2/issues/52
plugin "OAuth2" => {
google => {
key => $config->get( 'gapi/client_id' ), # $config->{gapi}{client_id},
secret => $config->get( 'gapi/client_secret' ), #$config->{gapi}{client_secret},
authorize_url => 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code',
token_url => 'https://www.googleapis.com/oauth2/v4/token' ## NB Google credentials.json specifies "https://www.googleapis.com/oauth2/v3/token"
}
};
# Marked for decomission
# helper get_email => sub {
# my ( $c, $access_token ) = #_;
# my %h = ( 'Authorization' => 'Bearer ' . $access_token );
# $c->ua->get( 'https://www.googleapis.com/auth/plus.profile.emails.read' => form => \%h )->res->json;
# };
helper get_new_tokens => sub {
my ( $c, $auth_code ) = #_;
my $hash = {};
$hash->{ code } = $c->param( 'code' );
$hash->{ redirect_uri } = $c->url_for->to_abs->to_string;
$hash->{ client_id } = $config->get( 'gapi/client_id' );
$hash->{ client_secret } = $config->get( 'gapi/client_secret' );
$hash->{ grant_type } = 'authorization_code';
my $tokens = $c->ua->post( 'https://www.googleapis.com/oauth2/v4/token' => form => $hash )->res->json;
return $tokens;
};
get "/" => sub {
my $c = shift;
$c->{ config } = $config;
app->log->info( "Will store tokens in" . $config->getFilename( $config->pathToFile ) );
if ( $c->param( 'code' ) ) ## postback from google
{
app->log->info( "Authorization code was retrieved: " . $c->param( 'code' ) );
my $tokens = $c->get_new_tokens( $c->param( 'code' ) );
app->log->info( "App got new tokens: " . Dumper $tokens);
if ( $tokens )
{
my $user_data;
if ( $tokens->{ id_token } )
{
# my $jwt = Mojo::JWT->new(claims => $tokens->{id_token});
# carp "Mojo header:".Dumper $jwt->header;
# my $keys = $c->get_all_google_jwk_keys(); # arrayref
# my ($header, $data) = decode_jwt( token => $tokens->{id_token}, decode_header => 1, key => '' ); # exctract kid
# carp "Decode header :".Dumper $header;
$user_data = decode_jwt( token => $tokens->{ id_token }, kid_keys => $c->ua->get( 'https://www.googleapis.com/oauth2/v3/certs' )->res->json, );
#carp "Decoded user data:" . Dumper $user_data;
}
#$user_data->{email};
#$user_data->{family_name}
#$user_data->{given_name}
# $tokensfile->set('tokens/'.$user_data->{email}, $tokens->{access_token});
$config->addToHash( 'gapi/tokens/' . $user_data->{ email }, 'access_token', $tokens->{ access_token } );
if ( $tokens->{ refresh_token } )
{
$config->addToHash( 'gapi/tokens/' . $user_data->{ email }, 'refresh_token', $tokens->{ refresh_token } );
}
else ## with access_type=offline set we should receive a refresh token unless user already has an active one.
{
carp('Google JWT Did not incude a refresh token - when the access token expires services will become inaccessible');
}
}
$c->render( json => $config->get( 'gapi' ) );
}
else ## PRESENT USER DEFAULT PAGE TO REQUEST GOOGLE AUTH'D ACCESS TO SERVICES
{
$c->render( template => 'oauth' );
}
};
app->secrets( ['putyourownsecretcookieseedhereforsecurity' . time] ); ## NB persistence cookies not required beyond server run
app->start( 'daemon', '-l', "http://*:$port" );
return 1;
}
## replacement for STDIN as per https://coderwall.com/p/l9-uvq/reading-from-stdin-the-good-way
sub _stdin
{
my $io;
my $string = q{};
$io = IO::Handle->new();
if ( $io->fdopen( fileno( STDIN ), 'r' ) )
{
$string = $io->getline();
$io->close();
}
chomp $string;
return $string;
}
=head2 TODO: Improve user interface of the HTML templates beneath DATA section
=over 1
=item * include Auth with Google button from Google Assets and advertise scopes reqeusted on the oauth.html
=item * More informative details on post-authentication page - perhaps include scopes, filename updated and instructions on revoking
=back
=cut
__DATA__
## oauth.html.ep
<%= link_to "Click here to get Google OAUTH2 tokens", $c->oauth2->auth_url("google",
authorize_query => { access_type => 'offline'},
scope => $c->{config}->get('gapi/scopes'), ## scope => "email profile https://www.googleapis.com/auth/plus.profile.emails.read https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/contacts.readonly",
)
%>
<br>
<br>
<a href="https://developers.google.com/+/web/api/rest/oauth#authorization-scopes">
Check more about authorization scopes</a>
Once you have a token in your gapi.json you can check the available scopes with curl using <pre>curl https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=<YOUR_ACCESS_TOKEN></pre>
__END__
Related
Using Nextjs and next-auth for everything authentication.
We've successfully integrated email (magic link), Facebook, and Google auth, but for some reason, Apple auth is still a real PITA.
I've set up the Provider, as usual:
AppleProvider({
clientId: String(process.env.APPLE_ID),
clientSecret: String(process.env.APPLE_SECRET),
profile(profile) {
return {
id: profile.sub,
name: profile.name,
firstName: profile.name.split(' ').slice(0, -1).join(' '), // We assume the first name is everything before the last word in the full name
lastName: profile.name.split(' ').slice(-1)[0], // We assume the last name is the last word in the full name
email: profile.email,
image: null,
}
},
}),
I have a SignIn callback ready to handle each one of those providers upon a successful authentication.
But upon a successful authentication, it doesn't even get to my callback, it shows the following error in the logs:
https://next-auth.js.org/errors#oauth_callback_error invalid_client {
error: {
message: 'invalid_client',
stack: 'OPError: invalid_client
' +
' at processResponse (/var/task/node_modules/openid-client/lib/helpers/process_response.js:45:13)
' +
' at Client.grant (/var/task/node_modules/openid-client/lib/client.js:1265:26)
' +
' at processTicksAndRejections (internal/process/task_queues.js:95:5)
' +
' at async Client.oauthCallback (/var/task/node_modules/openid-client/lib/client.js:561:24)
' +
' at async oAuthCallback (/var/task/node_modules/next-auth/core/lib/oauth/callback.js:114:16)
' +
' at async Object.callback (/var/task/node_modules/next-auth/core/routes/callback.js:50:11)
' +
' at async NextAuthHandler (/var/task/node_modules/next-auth/core/index.js:226:28)
' +
' at async NextAuthNextHandler (/var/task/node_modules/next-auth/next/index.js:16:19)
' +
' at async /var/task/node_modules/next-auth/next/index.js:52:32
' +
' at async Object.apiResolver (/var/task/node_modules/next/dist/server/api-utils.js:102:9)',
name: 'OPError'
},
providerId: 'apple',
message: 'invalid_client'
}
I tried visiting the error URL it's outputting (https://next-auth.js.org/errors#oauth_callback_error) but it wasn't helpful at all.
The whitelisted domains and return URLs are definitely all correct. They are the same for Google and Facebook.
My last guess is that I generated the clientSecret wrong. So here's how I did it:
I'm using the following Cli script:
#!/bin/node
import { SignJWT } from "jose"
import { createPrivateKey } from "crypto"
if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
Creates a JWT from the components found at Apple.
By default, the JWT has a 6 months expiry date.
Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
Usage:
node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
Options:
--help Print this help message
--kid, --key_id The key id of the private key
--iss, --team_id The Apple team ID
--private_key The private key to use to sign the JWT. (Starts with -----BEGIN PRIVATE KEY-----)
--sub, --client_id The client id to use in the JWT.
--expires_in Number of seconds from now when the JWT should expire. Defaults to 6 months.
--exp Future date in seconds when the JWT expires
`)
} else {
const args = process.argv.slice(2).reduce((acc, arg, i) => {
if (arg.match(/^--\w/)) {
const key = arg.replace(/^--/, "").toLowerCase()
acc[key] = process.argv[i + 3]
}
return acc
}, {})
const {
team_id,
iss = team_id,
private_key,
client_id,
sub = client_id,
key_id,
kid = key_id,
expires_in = 86400 * 180,
exp = Math.ceil(Date.now() / 1000) + expires_in,
} = args
/**
* How long is the secret valid in seconds.
* #default 15780000
*/
const expiresAt = Math.ceil(Date.now() / 1000) + expires_in
const expirationTime = exp ?? expiresAt
console.log(`
Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)}
${await new SignJWT({})
.setAudience("https://appleid.apple.com")
.setIssuer(iss)
.setIssuedAt()
.setExpirationTime(expirationTime)
.setSubject(sub)
.setProtectedHeader({ alg: "ES256", kid })
.sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`)
}
I've set up a Yarn script in my package.json for it so I can call it this way:
yarn apple-gen-secret --kid [OUR-APPLE-KEY-ID] --iss [OUR-APPLE-TEAM-ID] --private_key "[OUR-APPLE-AUTH-KEY]" --sub [OUR-APPLE-SERVICE-ID]
I totally forgot where I got this script from. But running it with the -h flag gives all the parameters it expects and why I'm using the specific command above.
Some time ago I've successfully integrated Superset authentication with Oauth using AWS Cognito.
Now I'm trying to do the same with Auth0, reusing the previous configuration and changing the endpoints according to Auth0 documentation.
Unfortunately, the login fails and Superset's log returns the following message:
2021-10-20 10:30:48,886:ERROR:flask_appbuilder.security.views:Error on OAuth authorize: request() got an unexpected keyword argument 'scope'
This is the Oauth configuration in superset_config.py:
from superset.security import SupersetSecurityManager
import json
import logging
logger = logging.getLogger(__name__)
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
if provider == 'auth0':
res = self.appbuilder.sm.oauth_remotes[provider].get('userinfo')
if res.raw.status != 200:
logger.error('Failed to obtain user info: %s', res.data)
return
me = json.loads(res._content)
logger.warning(" user_data: %s", me)
prefix = 'Superset'
logging.warning("user_data: {0}".format(me))
return {
'username' : me['email'],
'name' : me['name'],
'email' : me['email'],
'first_name': me['given_name'],
'last_name': me['family_name'],
}
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH0_URL = os.getenv('AUTH0_URL')
AUTH0_CLIENT_KEY = os.getenv('AUTH0_CLIENT_KEY')
AUTH0_CLIENT_SECRET = os.getenv('AUTH0_CLIENT_SECRET')
OAUTH_PROVIDERS = [{
'name':'auth0',
'token_key': 'access_token',
'icon':'fa-at',
'url': AUTH0_URL,
'remote_app': {
'client_id': AUTH0_CLIENT_KEY,
'client_secret': AUTH0_CLIENT_SECRET,
'request_token_params': {
'scope': 'email openid profile'
},
'response_type': 'token_id',
'base_url': AUTH0_URL,
'access_token_url': os.path.join(AUTH0_URL, 'oauth/token'),
'authorize_url': os.path.join(AUTH0_URL, 'authorize'),
'access_token_method':'POST',
'request_token_url': os.path.join(AUTH0_URL, 'oauth/token'),
'api_base_url': AUTH0_URL,
}
}
]
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
I have already tried different values for the response_type (code, token, token_id).
Also tried to leave request_token_url empty and in that case the error changes because the user data appear to be an empty dictionary:
2021-10-13 15:52:10,358:WARNING:superset_config: user_data: {}
2021-10-13 15:52:10,358:WARNING:root:user_data: {}
2021-10-13 15:52:10,358:ERROR:flask_appbuilder.security.views:Error returning OAuth user info: 'email'
So I assume the token is actually returned and I cannot understand why Flask is complaining about the attribute "scope".
Tried this too, since it looked like very similar to my problem, but none of those configurations work for me.
Hope you have two files as custom_sso_security_manager.py and superset_config.py
Can you remove below two line from return and try(custom_sso_security_manager.py).
'first_name': me['given_name'],
'last_name': me['family_name'],
This is for future reference, although I accepted Kamal's answer.
It turned out that the right parameter to set the request token scopes was client_kwargs instead of request_token_params.
This is a working configuration to authenticate Superset against Auth0:
## Enable OAuth authentication
from flask_appbuilder.security.manager import (
AUTH_OAUTH,
)
from superset.security import SupersetSecurityManager
import json
import logging
import string
import random
nonce = ''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k = 30))
logger = logging.getLogger(__name__)
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
if provider == 'auth0':
res = self.appbuilder.sm.oauth_remotes[provider].get('userinfo')
if res.raw.status != 200:
logger.error('Failed to obtain user info: %s', res.json())
return
me = res.json()
return {
'username' : me['email'],
'name' : me['name'],
'email' : me['email'],
}
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH0_URL = os.getenv('AUTH0_URL')
AUTH0_CLIENT_KEY = os.getenv('AUTH0_CLIENT_KEY')
AUTH0_CLIENT_SECRET = os.getenv('AUTH0_CLIENT_SECRET')
OAUTH_PROVIDERS = [
{ 'name':'auth0',
'token_key':'access_token',
'icon':'fa-at',
'remote_app': {
'api_base_url': AUTH0_URL,
'client_id': AUTH0_CLIENT_KEY,
'client_secret': AUTH0_CLIENT_SECRET,
'server_metadata_url': os.path.join(AUTH0_URL, '.well-known/openid-configuration'),
'client_kwargs': {
'scope': 'openid profile email'
},
'response_type': 'code token',
'nonce': nonce,
}
}
]
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
As per Flask Documentation,
try to use client_kwargs instead of request_token_params key.
Sample:
{
'name':'google',
'icon':'fa-google',
'token_key':'access_token',
'remote_app': {
'client_id':'GOOGLE_KEY',
'client_secret':'GOOGLE_SECRET',
'api_base_url':'https://www.googleapis.com/oauth2/v2/',
'client_kwargs':{
'scope': 'email profile'
},
'request_token_url':None,
'access_token_url':'https://accounts.google.com/o/oauth2/token',
'authorize_url':'https://accounts.google.com/o/oauth2/auth'
}
},
I am currently working on the project that I am going to integrate the application of my company and salesforce.
In my case, it seemed that using the JWT for authentication is better. So, I wanted to try it.
but I don't know how to generate JWT and send the proper request to salesforce on Ruby though I read docs.
What I wanted to do is that
1, create application on salesforce (done)
2, create X509 certification and set it on the application on salesforce. (done)
3, create JWT by using the secret key of X509 certification. (I think I've done it )
4, send post request with JWT parameter included in assertion params and grant_type(grant_type= urn:ietf:params:oauth:grant-type:jwt-bearer&) (I got an error)
when I send post request the errors says {"error":"invalid_grant","error_description":"invalid assertion"} so it occurs certainly because of the parameter I sent.
the code I randomly wrote is something like this.
require 'jwt'
require 'json'
require 'net/http'
require 'uri'
payload = {
"sub": "abel#example.com", ← my account on salesforce
"iss": "3MVG9pe2TCoA1PasbdvjabsodyoQFZTn0Rjsdbfjbasojdbn;oajs", ← the consumer key of the application on salesforce.
"aud": "https://test.salesforce.com"
}
public_key = Base64.urlsafe_encode64(
'USqTxNC7MMIeF9iegop3WeDvFL
075JSUECgYEA76FNJLeStdq+J6Fj2ZBYdDoeuDHv3iNA0nnIse9d6HnjbdrdvjmV
rT1CJuHh9gnNKg4tyjkbpc9IVj4/GF0mNUCgYEAynvj
qOYCzts4W7Bdumk6z8QULJ5QoYCrAgFtwra9R1HDcxTz+GPgJOVx2QBX+aQbDOaD
WV1s9WqE0/Lfi/VVUEzg1hZ8326buGRk1DRVG2Oa48==') ← this is public_key example of the certification.
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key
token = JWT.encode payload, rsa_private, 'RS256'
puts token
decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
puts decoded_token
post = {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': token
}
uri = URI.parse('https://login.salesforce.com/services/oauth2/token')
https = Net::HTTP.new(uri.host, 443)
https.use_ssl = true
response = https.post(uri.path, post.to_query)
print response.body
the PHP version of what I want to achieve is something like this.
<?php
require_once './vendor/autoload.php';
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Rsa\Sha256;
// login URL
// production: https://login.salesforce.com
// Sandbox: https://test.login.salesforce.com
define('LOGIN_URL', 'https://test.salesforce.com');
//consumer key
define('CLIENT_ID', <<consumer key of the application on salesforce>>);
//user ID
define('USER_ID', 'xxxxx#example.com');
function createjwt() {
$signer = new Sha256();
$privateKey = new Key('file://cert/server.key'); ← probably the key from certification
$time = time();
$token = (new Builder())->issuedBy(CLIENT_ID) // iss: consumer key
->permittedFor(LOGIN_URL) // aud: Salesforce login URL
->relatedTo(USER_ID) // sub: Salesforce user ID
->expiresAt($time + 3 * 60) // exp: within three mins
->getToken($signer, $privateKey);
return $token;
}
$jwt = createjwt();
echo $jwt;
function auth() {
$jwt = createjwt();
$post = array(
'grant_type' => GRANT_TYPE,
'assertion' => $jwt,
);
$curl = curl_init();
curl_setopt( $curl, CURLOPT_URL, AUTH_URL );
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
curl_setopt( $curl, CURLOPT_POSTFIELDS, $post );
$buf = curl_exec( $curl );
if ( curl_errno( $curl ) ) {
exit;
}
curl_close( $curl );
$json = json_decode( $buf );
$accinfo = array(
// URL to access
'instance_url' => $json->instance_url,
// Bearer token in order to access
'access_token' => $json->access_token,
);
return $accinfo;
}
$accinfo = auth();
EDIT
I changed a code a lot. But I still have different error that says 'initialize': Neither PUB key nor PRIV key: nested asn1 error (OpenSSL::PKey::RSAError)' around #private_key definition.
I read this and tried changing the string in private_key.pem to in one line but I didn't work ( maybe I did in a wrong way) and didn't understand the meaning of incorrect password (mentioned as the second answer) What causes "Neither PUB key nor PRIV key:: nested asn1 error" when building a public key in ruby?
def initialize
#cert_file = File.join(File.dirname(__FILE__), *%w[private_key.pem])
# #cert = Base64.urlsafe_encode64(#cert_file)
# print #cert_
# #cert_file = File.join(File.dirname(__FILE__), *%w[server.csr])
#base_url = "https://test.salesforce.com"
#auth_endpoint = "/services/oauth2/authorize"
#token_request_endpoint = "/services/oauth2/token"
#token_revoke_endpoint = "/services/oauth2/revoke"
#username = "my username"
#client_id = "pe2TCoA1~~~~" client_id
#private_key = OpenSSL::PKey::RSA.new(File.read(#cert_file))
# #private_key = OpenSSL::PKey::RSA.generate(private_key)
#rsa_public = #private_key.public_key
# #private_key = OpenSSL::PKey::RSA.new(File.read(#cert_file))
end
def claim_set
{
iss: #client_id,
sub: #username,
aud: #base_url,
exp: (Time.now + 3.minutes).to_i.to_s
}
end
def jwt_bearer_token
JWT.encode(self.claim_set.to_s, #rsa_public, 'RS256')
end
def request_auth
post = {body: {grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt_bearer_token}}
uri = URI.parse("#{#base_url}#{#token_request_endpoint}")
https = Net::HTTP.new(uri.host, 443)
https.use_ssl = true
response = https.post(uri.path, post.to_query)
print response.body
end
Salesforce.new.request_auth
end
Any advices are appreciated.
Thank you
OAuth2 is producing "Username and Password not accepted" error when try to send email with Gmail+ Nodejs+Nodemailer
Code - Nodejs - Nodemailer and xoauth2
var nodemailer = require("nodemailer");
var generator = require('xoauth2').createXOAuth2Generator({
user: "", // Your gmail address.
clientId: "",
clientSecret: "",
refreshToken: "",
});
// listen for token updates
// you probably want to store these to a db
generator.on('token', function(token){
console.log('New token for %s: %s', token.user, token.accessToken);
});
// login
var smtpTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
xoauth2: generator
}
});
var mailOptions = {
to: "",
subject: 'Hello ', // Subject line
text: 'Hello world ', // plaintext body
html: '<b>Hello world </b>' // html body
};
smtpTransport.sendMail(mailOptions, function(error, info) {
if (error) {
console.log(error);
} else {
console.log('Message sent: ' + info.response);
}
smtpTransport.close();
});
issues:
I used Google OAuth2 playground to create the tokens, https://developers.google.com/oauthplayground/
It looks to grab a valid accessToken ok, using the refreshToken, (i.e. it prints the new access token on the screen.) No errors until it tries to send the email.
I added the optional accessToken: but got the same error. ( "Username and Password not accepted")
I am not 100% sure about the "username", the docs say it needs a "user" email address - I guess the email of the account that created to token, but is not 100% clear. I have tried several things and none worked.
I have searched the options on the gmail accounts, did not find anything that looks wrong.
Also, when I did this with Java, it needed the google userID rather than the email address, not sure why this is using the email address and the Java is using the UserId.
nodemailer fails with a "compose" scope
The problem was the "scope"
it fails with:
https://www.googleapis.com/auth/gmail.compose
but works ok if I use
https://mail.google.com/
Simply just do the following:
1- Get credentials.json file from here https://developers.google.com/gmail/api/quickstart/nodejs press enable the Gmail API and then choose Desktop app
2- Save this file somewhere along with your credentials file
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');
// If modifying these scopes, delete token.json.
const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';
// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
if(err){
return console.log('Error loading client secret file:', err);
}
// Authorize the client with credentials, then call the Gmail API.
authorize(JSON.parse(content), getAuth);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
const {client_secret, client_id, redirect_uris} = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, (err, token) => {
if(err){
return getNewToken(oAuth2Client, callback);
}
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
* #param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback for the authorized client.
*/
function getNewToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
callback(oAuth2Client);
});
});
}
function getAuth(auth){
}
3 - Run this file by typing in your terminal: node THIS_FILE.js
4- You'll have token.json file
5- take user information from credentials.json and token.json and fill them in the following function
const nodemailer = require('nodemailer');
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;
const email = 'gmail email'
const clientId = ''
const clientSecret = ''
const refresh = ''
const oauth2Client = new OAuth2(
clientId,
clientSecret,
);
oauth2Client.setCredentials({
refresh_token: refresh
});
const newAccessToken = oauth2Client.getAccessToken()
let transporter = nodemailer.createTransport(
{
service: 'Gmail',
auth: {
type: 'OAuth2',
user: email,
clientId: clientId,
clientSecret: clientSecret,
refreshToken: refresh,
accessToken: newAccessToken
}
},
{
// default message fields
// sender info
from: 'Firstname Lastname <your gmail email>'
}
);
const mailOptions = {
from: email,
to: "",
subject: "Node.js Email with Secure OAuth",
generateTextFromHTML: true,
html: "<b>test</b>"
};
transporter.sendMail(mailOptions, (error, response) => {
error ? console.log(error) : console.log(response);
transporter.close();
});
If your problem is the scopes, here is some help to fix
Tried to add this as an edit to the top answer but it was rejected, don't really know why this is off topic?
See the note here: https://nodemailer.com/smtp/oauth2/#troubleshooting
How to modify the scopes
The scopes are baked into the authorization step when you get your first refresh_token. If you are generating your refresh token via code (for example using the Node.js sample) then the revised scope needs to be set when you request your authUrl.
For the Node.js sample you need to modify SCOPES:
// If modifying these scopes, delete token.json.
-const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
+const SCOPES = ['https://mail.google.com'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
And then the call to oAuth2Client.generateAuthUrl will produce a url that will request authorization from the user to accept full access.
from the Node.js sample:
function getNewToken(oAuth2Client, callback) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
I am currently trying to upgrade my application to send an OAuth token through as a header instead of through the query string as per their new requirements. When making a GET request with the OAuth token in the header, my request succeeds verifying a valid access_token. However when trying to make a post with the same token, I receive a 401 unauthorized. This post with the same access token succeeds when the access token is placed on the query string.
var request = (HttpWebRequest)WebRequest.Create(yammerurl);
request.Method = "POST";
request.Headers["Authorization"] = "Bearer " + access_token;
request.Host = "www.yammer.com";
request.ContentType = "application/json;charset=utf-8";
This is my set up for posting that is receiving an unauthorized exception and below is my set up for the GET request that succeeds. Again both of them are using the same access token and both methods work when the access token is passed through the query string.
string url = "https://www.yammer.com/api/v1/groups.json?mine=1";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Headers["Authorization"] = "Bearer " + YammerAccessToken;
request.Host = "www.yammer.com";
Does anybody know if my setup for posting is incorrect or if there is another parameter I need to add?
Is it because you are not specifying the HTTP verb GET in the second one? This is how I accomplish it in PHP and it works fine...
I call this in my main php file...
$ymuser = yammer_user_by_email('myemail#test.com');
this function is in my inc file...
function yammer_user_by_email($email, $token = null){
global $YAMMER_ADMIN_TOKEN;
$user = yammer_api_get('https://www.yammer.com/api/v1/users/by_email.json?email='.$email, $YAMMER_ADMIN_TOKEN);
return $user[0];
}
All of my http calls are routed through here, and the admin token is applied to the header...
function yammer_api_call($url, $method = 'GET', $body = '', $token){
if ($token == null) {
if (!$_SESSION['yammer_token'] || !$_SESSION['yammer_token']->access_token->token) return false;
$token = $_SESSION['yammer_token']->access_token->token;
}
if ($method == 'GET'){
$opts = array('http' =>
array(
'method' => $method,
'header' => "Host: www.yammer.com\r\n"
."Authorization: Bearer " . $token . "\r\n"
)
);
}else{
$opts = array('http' =>
array(
'method' => $method,
'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
."Host: www.yammer.com\r\n"
."Authorization: Bearer " . $token . "\r\n"
."Content-Length: " . strlen($body) . "\r\n",
'content' => $body,
'timeout' => 60
)
);
}
$context = stream_context_create($opts);
$resp = file_get_contents($url, false, $context);
//print($resp);
$resp_obj = json_decode($resp);
return $resp_obj;
}