Authentication for Google API - javascript

I'm trying to understand the flow how to authenticate user on WEB client (JS), and then use Google API on my back-end server (ASP.NET MVC application), on behalf of authenticated user for retrieving users contacts list.
Here the current flow that I use:
1.In HTML I use google JS client: https://apis.google.com/js/client.js:
function auth(callback) {
var config = {
'client_id': '***********',
'scope': 'https://www.googleapis.com/auth/contacts.readonly'
};
config.immediate = true;
gapi.auth.authorize(config, function (authResult) {
if (authResult && !authResult.error) {
callback();
}
else {
config.immediate = false;
gapi.auth.authorize(config, function (response) {
//Here I send access_token to back-end using HTTPS
});
}
});
}
2.Then I use gapi.auth.getToken() and send it to back-end server (Using a HTTPS AJAX call)
3.Then on server I have the following code in controller:
public JsonResult Get(TokenModel model)
{
//Custom store for access_token
var myStore = new MyStore(NewtonsoftJsonSerializer.Instance.Serialize(new TokenResponse() { Issued = DateTime.Now, ExpiresInSeconds = 3600, TokenType = "Bearer", AccessToken = model.access_token }));
string[] Scopes = { PeopleService.Scope.ContactsReadonly };
ClientSecrets secrets = new ClientSecrets() { ClientId = "******", ClientSecret = "******" };
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
secrets,
Scopes,
"user",
CancellationToken.None,
myStore
).Result;
var service = new PeopleService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
List<string> result = GetPeople(service, null);
return Json(result);
}
Questions:
Is it the correct flow and does GoogleWebAuthorizationBroker is a correct class to use on server in my case?
Why and HOW GoogleWebAuthorizationBroker opens a new browser window for authentication, in case model.access_token = null?
Why when the token is not valid (ex: “dasdasdasdas”), AuthorizeAsync method returns me the UserCredential that looks absolutely valid, but then the exception occurs when make actual request to google api.
How from the above flow, I can get “refresh token” for later use (as I understand, I need somehow generate it myself, using access_token + secret key).
Thanks!

Related

JWT token stored in localstorage lost after window.location.href call

I am using Javascript Fetch API to invoke Dot net web API.
Using Visual Studio code for HTML/Javascript & Visual Studio 2019 for Dot Net Web API.
I am trying to implement login functionality using Dot net C# Web API, that will return JWT token in the response and use the token later to invoke separate Web API/Service (e.g. EmployeeInfo ).
Login page :
It displays user id and password fields and the button "Login"
Once user clicks on login button, the function fnlogin is invoked
function fnlogin() {
const uname = document.getElementById('uname').value;
const pwd = document.getElementById('pwd').value;
const logindata = {
username: uname,
password: pwd
}
const loginurl = 'http://localhost:13402/api/Auth/Login';
authenticate(loginurl, logindata);
}
async function authenticate(loginurl, logindata) {
console.log(logindata)
const response = await fetch(loginurl , {
method: "POST",
mode: "cors",
body: JSON.stringify(logindata),
headers: { "Content-type" : "application/json, charset=UTF-8"}
});
const rdata = await response.json();
console.log(rdata);
if (!rdata.success) {
document.getElementById("loginMessage").innerHTML = rdata.message;
return;
}
const inMemoryToken = rdata.data
localStorage.setItem('user', JSON.stringify(rdata));
window.location.href = "http://localhost:5500/Employeeinfo1.html";
}
The Web API returns JWT token properly.
The code in controller is as follows :
[HttpPost("Login")]
public async Task<ActionResult<ServiceResponse<string>>> Login(UserLoginDto request)
{
var response = await _authRepo.Login(
request.Username, request.Password
);
if (!response.Success)
{
return BadRequest(response);
}
return Ok(response);
}
``
The code in AuthRepository class is as follows :
public class AuthRepository : IAuthRepository
{
private readonly AppDbContext _context;
private readonly IConfiguration _configuration;
public AuthRepository(AppDbContext context, IConfiguration configuration)
{
_configuration = configuration;
_context = context;
}
public async Task<ServiceResponse<string>> Login(string username, string password)
{
var response = new ServiceResponse<string>();
var user = await _context.Users.FirstOrDefaultAsync(x => x.Username.ToLower().Equals(username.ToLower()));
if (user == null)
{
response.Success = false;
response.Message = "User not found.";
}
else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
{
response.Success = false;
response.Message = "Wrong password.";
}
else
{
response.Data = CreateToken(user);
}
return response;
}
public async Task<ServiceResponse<User>> Register(User user, string password)
{
ServiceResponse<User> response = new ServiceResponse<User>();
if (await UserExists(user.Username))
{
response.Success = false;
response.Message = "User already exists.";
return response;
}
CreatePasswordHash(password, out byte[] passwordHash, out byte[] passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
await _context.SaveChangesAsync();
response.Data = user;
return response;
}
public async Task<bool> UserExists(string username)
{
if (await _context.Users.AnyAsync(x => x.Username.ToLower().Equals(username.ToLower())))
{
return true;
}
return false;
}
private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
using (var hmac = new System.Security.Cryptography.HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt)
{
using (var hmac = new System.Security.Cryptography.HMACSHA512(passwordSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != passwordHash[i])
{
return false;
}
}
return true;
}
}
private string CreateToken(User user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username)
};
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration.GetSection("AppSettings:Token").Value));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokendDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = System.DateTime.Now.AddDays(1),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokendDescriptor);
return tokenHandler.WriteToken(token);
}
4. Once the JWT token is returned , it is stored in LocalStorage. I verified in Chrome Dev Tools
But after the next screen is displayed, when I check in Chrome dev tools, the object/token is no longer present in the Local Storage.
Is this because of window.location.href will remove all local data in localStorage in the browser ?
I want to be able to use the token to pass to remaining html screens and web api
You are storing an item in the local storage of http://127.0.0.1:5500 and trying to read it from http://localhost:5500.
From Window.localStorage in MDN Web Docs:
The localStorage read-only property of the window interface allows you to access a Storage object for the Document's origin;
And about Origin:
Web content's origin is defined by the scheme (protocol), host (domain), and port of the URL used to access it. Two objects have the same origin only when the scheme, host, and port all match.
As you see, for the browser the origins are different because the host does not match: 127.0.0.1 is not the same string as localhost.
Use either localhost or 127.0.0.1 everywhere, but do not use both.

About how the value is returned using app.set() and app.get()

I am releasing access to pages using connect-roles and loopback but I have a pertinent question about how I can collect the customer's role and through the connect-roles to read the session and respond to a route.
Example, when the client logs in I load a string containing the client's role and access it in a function that controls access to pages.
I have this doubt because I'm finalizing a large scale service that usually there are multiple client sessions that are accessed instantly using a same storage and check function.
It would be efficient to store the customer's role using app.set() and app.get()?
app.get('/session-details', function (req, res) {
var AccessToken = app.models.AccessToken;
AccessToken.findForRequest(req, {}, function (aux, accesstoken) {
// console.log(aux, accesstoken);
if (accesstoken == undefined) {
res.status(401);
res.send({
'Error': 'Unauthorized',
'Message': 'You need to be authenticated to access this endpoint'
});
} else {
var UserModel = app.models.user;
UserModel.findById(accesstoken.userId, function (err, user) {
// console.log(user);
res.status(200);
res.json(user);
// storage employee role
app.set('employeeRole', user.accessLevel);
});
}
});
});
Until that moment everything happens as desired I collect the string loaded with the role of the client and soon after I create a connect-roles function to validate all this.
var dsConfig = require('../datasources.json');
var path = require('path');
module.exports = function (app) {
var User = app.models.user;
var ConnectRoles = require('connect-roles');
const employeeFunction = 'Developer';
var user = new ConnectRoles({
failureHandler: function (req, res, action) {
// optional function to customise code that runs when
// user fails authorisation
var accept = req.headers.accept || '';
res.status(403);
if (~accept.indexOf('ejs')) {
res.send('Access Denied - You don\'t have permission to: ' + action);
} else {
res.render('access-denied', {action: action});
// here
console.log(app.get('employeeRole'));
}
}
});
user.use('authorize access private page', function (req) {
if (employeeFunction === 'Manager') {
return true;
}
});
app.get('/private/page', user.can('authorize access private page'), function (req, res) {
res.render('channel-new');
});
app.use(user.middleware());
};
Look especially at this moment, when I use the
console.log(app.get('employeeRole')); will not I have problems with simultaneous connections?
app.get('/private/page', user.can('authorize access private page'), function (req, res) {
res.render('channel-new');
});
Example client x and y connect at the same time and use the same function to store data about your session?
Being more specific when I print the string in the console.log(app.get('employeeRole')); if correct my doubt, that I have no problem with simultaneous connections I will load a new variable var employeeFunction = app.get('employeeRole'); so yes my function can use the object containing the role of my client in if (employeeFunction === 'Any Role') if the role that is loaded in the string contain the required role the route it frees the page otherwise it uses the callback of failureHandler.
My test environment is limited to this type of test so I hope you help me on this xD
Instead of using app.set you can create a session map(like hashmaps). I have integrated the same in one of my projects and it is working flawlessly. Below is the code for it and how you can access it:
hashmap.js
var hashmapSession = {};
exports.auth = auth = {
set : function(key, value){
hashmapSession[key] = value;
},
get : function(key){
return hashmapSession[key];
},
delete : function(key){
delete hashmapSession[key];
},
all : function(){
return hashmapSession;
}
};
app.js
var hashmap = require('./hashmap');
var testObj = { id : 1, name : "john doe" };
hashmap.auth.set('employeeRole', testObj);
hashmap.auth.get('employeeRole');
hashmap.auth.all();
hashmap.auth.delete('employeeRole');

"The AWS Access Key Id you provided does not exist in our records" during a federation with Salesforce

I'm trying to establish a federation among Amazon and Salesforce, in this way: if a user correctly authenticates through Salesforce it will see all S3 buckets in the given account.
Quite simple, I followed this blog post and changed something (i.e. I don't use a DyanamoDb table and the callback is for simplicity inside an S3 bucket). The flow that I'm trying to implement is called Enhanced (simplified) flow (details here):
I slightly modified the callback code compared to the article:
function onPageLoad() {
var url = window.location.href;
var match = url.match('id_token=([^&]*)');
var id_token = "";
if (match) {
id_token = match[1];
} else {
console.error("Impossibile recuperare il token");
}
AWS.config.region = "eu-west-1"
const cognitoParams = {
AccountId: "ACC_ID",
IdentityPoolId: "eu-west-1:XXX",
Logins: {
"login.salesforce.com": id_token
}
}
const identity = new AWS.CognitoIdentity()
identity.getId(cognitoParams, function (err, identityData) {
if (err) {
printMessage(err);
return;
}
const identityParams = {
IdentityId: identityData.IdentityId,
Logins: cognitoParams.Logins
}
identity.getCredentialsForIdentity(identityParams, function (err, data) {
if (err) {
printMessage(err);
} else {
var c = {
region: 'eu-west-1',
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretKey
};
var s3 = new AWS.S3(c);
// HERE IS THE ERRORE - data is empty and response contains the error
s3.listBuckets((response, data) => {
data.Buckets.forEach(function (value) { appendMessage(value.Name) })
});
}
});
});
// IRRELEVANT CODE
}
I can get the token from Salesforce, I can get the access and secret keys but when I try to list the buckets I get a laconic:
The AWS Access Key Id you provided does not exist in our records.
I found this error reasonable since I have no user at all and the keys are created on-the-fly. Where can I hit my head? The SDK is 2.103.0.
Could be due to eventual consistency of IAM, can you try to include a delay before calling the listbucket api or make the request to us-east-1 endpoint?
http://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_access-denied-service2.
GetCredentialsForIdentity returns temporary credentials. So you should include AccessKeyId, SecretKey and SessionToken to make the request.
http://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html
Hope this helps.

How to manage API permissions? javascript

I've written some client-side app and tried to test it. How it turned out only I can use it. Anyone else will get such error.
{
"error": {
"errors": [
{
"domain": "global",
"reason": "forbidden",
"message": "Forbidden"
}
],
"code": 403,
"message": "Forbidden"
}
}
What does it mean? How to solve this?
There is my code. There i'm getting Email, name, surname and user photo. I want to get the number of youtube channel subscribers and work with youtube later. For example I want to rate some videos directly from the site.
function resultFindUserByEmail()
{
if (ajaxRet['isUserFinded'])
{
cf_JSON.clear();
cf_JSON.addItem( 'email',email );
var jsonstr = cf_JSON.make();
ajax_post('doyoutubelogin','loginres','index.php',jsonstr,c_dologin);
}else{
gapi.client.init({
discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/people/v1/rest"],
clientId: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES
}).then(function () {
var request = gapi.client.people.people.get({
'resourceName': 'people/me'
}).then(function(response) {
var parsedResponse = JSON.parse(response.body).names;
surname = parsedResponse[0].familyName;
name = parsedResponse[0].givenName;
photo = JSON.parse(response.body).photos[0].url;
addYoutubeUser();
});
});
}
}
function addYoutubeUser() {
cf_JSON.clear();
cf_JSON.addItem( 'Email',email );
cf_JSON.addItem( 'Firstname',name );
cf_JSON.addItem( 'Lastname',surname );
cf_JSON.addItem( 'Image',photo );
var jsonstr = cf_JSON.make();
ajax_post('addyoutubeuser','loginres','index.php',jsonstr,c_dologin);
}
var API_KEY = '<Key removed for posting>';
var API_KEY1='<Key removed for posting>';
var OAUTH2_CLIENT_ID = '<Key removed for posting>';
var OAUTH2_CLIENT_ID1 = '<Key removed for posting>';
var OAUTH2_SCOPES = 'https://www.googleapis.com/auth/youtube.force-ssl';
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"];
var GoogleAuth;
function handleClientLoad() {
// Load the API's client and auth2 modules.
// Call the initClient function after the modules load.
gapi.load('client:auth2', initClient);
}
function initClient() {
// Retrieve the discovery document for version 3 of YouTube Data API.
// In practice, your app can retrieve one or more discovery documents.
var discoveryUrl = 'https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest';
// Initialize the gapi.client object, which app uses to make API requests.
// Get API key and client ID from API Console.
// 'scope' field specifies space-delimited list of access scopes.
gapi.client.init({
'apiKey': API_KEY,
'discoveryDocs': [discoveryUrl,"https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"],
'clientId': OAUTH2_CLIENT_ID,
'scope': OAUTH2_SCOPES
}).then(function () {
GoogleAuth = gapi.auth2.getAuthInstance();
//GoogleAuth.grant(OAUTH2_SCOPES);
// Listen for sign-in state changes.
GoogleAuth.isSignedIn.listen(updateSigninStatus);
// Handle initial sign-in state. (Determine if user is already signed in.)
var user = GoogleAuth.currentUser.get();
setSigninStatus();
// Call handleAuthClick function when user clicks on
// "Sign In/Authorize" button.
$('#sign-in-or-out-button').click(function() {
handleAuthClick();
});
$('#revoke-access-button').click(function() {
revokeAccess();
});
});
}
function handleAuthClick() {
if (GoogleAuth.isSignedIn.get()) {
// User is authorized and has clicked 'Sign out' button.
GoogleAuth.signOut();
} else {
// User is not signed in. Start Google auth flow.
GoogleAuth.signIn();
}
}
function revokeAccess() {
GoogleAuth.disconnect();
}
function setSigninStatus(isSignedIn) {
var user = GoogleAuth.currentUser.get();
var isAuthorized = user.hasGrantedScopes(OAUTH2_SCOPES);
if (isAuthorized) {
$('#sign-in-or-out-button').html('Sign out');
$('#revoke-access-button').css('display', 'inline-block');
$('#auth-status').html('You are currently signed in and have granted ' +
'access to this app.');
//// get gmail Email
gapi.client.init({
'apiKey': API_KEY,
'discoveryDocs': ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"],
'clientId': OAUTH2_CLIENT_ID,
'scope': OAUTH2_SCOPES
}).then(function () {
var request = gapi.client.gmail.users.getProfile({
'userId': 'me'
}).then(function(response) {
email = JSON.parse(response.body).emailAddress;
cf_JSON.clear();
cf_JSON.addItem( 'email',email );
var jsonstr = cf_JSON.make();
tryFindUserByEmail(jsonstr);
});
});
// try to find email
} else {
$('#sign-in-or-out-button').html('Вход через Youtube');
$('#revoke-access-button').css('display', 'none');
$('#auth-status').html('You have not authorized this app or you are ' +
'signed out.');
}
}
function updateSigninStatus(isSignedIn) {
setSigninStatus();
}
How to manage permissions:
When you authenticate a user you are given access to that users account data and only that user. So if you are trying to access data on someone else's account they are not going to have permissions to access it and you are going to get the 403 forbidden error.
Without seeing your code its hard to know what you are doing, but I can guess.
You are using Oauth2 to authenticate users.
You are trying to access something with a hard coded id belonging to your personal account which the user does not have access.
How to fix it will depend on what it is you are trying to do.
You need to check some authentication in the API url like
username , ipaddress , token etc.
Based on the parameter you can control the permission on your API request.for example
http://some/thing?username="testuser"&ipaddress="323.2323.232.32"
You can find the parameters value using the function below
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
And then make you check and implement your error and redirection for specific users.
I guess it will help full for you , Thanks !

Azure Mobile Services - Getting more user information

I inherited a Windows 8 application that is written with XAML. So in C# when I make this call
user = await MobileServices.MobileService
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);
(This is for Azure Mobile Services)
The user object is ONLY giving me the Token and the MicrosoftAccount:..............
In order to get to authenticate people, I need to be able to see WHO is requesting access...
I looking at articles like below, but I seem to be missing something? Is this javascript in the article something I would have to write in Node.js?
Example article:
http://blogs.msdn.com/b/carlosfigueira/archive/2013/12/12/expanded-login-scopes-in-azure-mobile-services.aspx
Currently to be able to get more information about the logged in user, you need to make a second call to the service to retrieve the user info. You don't really need to ask for additional login scopes (the topic of the post you mentioned) to retrieve the user name, since that is given by default for all the providers.
This post should have the code you need to write in the server side (node.js) to get more information about the logged in user. The TL;DR version is given below:
On the server side: add this custom API (I'll call it "userInfo"; set the permission of GET to "user", and all others to admin):
exports.get = function(request, response) {
var user = request.user;
user.getIdentities({
success: function(identities) {
var accessToken = identities.microsoft.accessToken;
var url = 'https://apis.live.net/v5.0/me/?method=GET&access_token=' + accessToken;
var requestCallback = function (err, resp, body) {
if (err || resp.statusCode !== 200) {
console.error('Error sending data to the provider: ', err);
response.send(statusCodes.INTERNAL_SERVER_ERROR, body);
} else {
try {
var userData = JSON.parse(body);
response.send(200, userData);
} catch (ex) {
console.error('Error parsing response from the provider API: ', ex);
response.send(statusCodes.INTERNAL_SERVER_ERROR, ex);
}
}
}
var req = require('request');
var reqOptions = {
uri: url,
headers: { Accept: "application/json" }
};
req(reqOptions, requestCallback);
}
});
}
On the client side, after a successful login, call that API:
user = await MobileServices.MobileService
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);
var userInfo = await MobileServices.MobileService.InvokeApiAsync(
"userInfo", HttpMethod.Get, null);
userInfo will contain a JObject with the user information. There is an open feature request to make this better at http://feedback.azure.com/forums/216254-mobile-services/suggestions/5211616-ability-to-intercept-the-login-response.

Categories