I'm building an application in Google Apps Script. I'm authenticating the domain users by checking if their logonid is permitted to use the application.
I developed it and when I entered the testing phase, I was the only user that could actually use the application.
Although I had set the "Who has Access" combobox on the web-app publish wizard to "anyone".
When a user executes the application he will get an exception saying he has no access when executing the first function after doGet(). Did I overlook some settings or did I do something wrong?
I use the following classes:
UserManager
Session
Jdbc
Utilities
Logger
This is the function that is called after the doGet() function:
function authenticateUser() {
try {
var user = UserManager.getUser(Session.getActiveUser());
Logger.log('User: ' + user.getEmail());
if (user == undefined || user == null) {
return {authenticated: false};
} else {
var auth = _getAuth();
if (!auth.isAuthorized(user.getEmail())) {
Logger.log('Not authorized in database.');
return {authenticated: false};
} else {
var profile = auth.getProfile(user.getEmail());
authenticated = true;
auth.setLogin(user.getEmail());
if(!profile.firstLogin) {
activeProfile = profile;
}
activeUser = user;
return {profile: profile, authenticated: true};
}
}
} catch(ex) {
Logger.log(ex);
return {authenticated: false};
}
}
Access to UserManager is the issue here. As the documentation states, UserManager is only accessible to administrators.
If the app is running as a normal user, it cannot access UserManager. You may need to rethnk the deployment/code to run it as yourselves (or admin).
Related
I have an Net Framework 4.5.2 project using MVC. I am using SignInManager.ExternalSignInAsync to checking windows domain users for an automatic login.
One user consistently returns Failure, and I'm trying to understand why. I've checked the ASPNetUsers, ASPNetLogins and ASPNetRoles tables and they are populated.
I use AuthenticationManager.GetExternalLoginInfoAsync() to retrieve the windows domain login, and that works fine for this user
Can anyone tell me why SignInManager.ExternalSignInAsync returns a failure on a login that just came from AuthenticationManager.GetExternalLoginInfoAsync()? Is there any way to ask SignInManager why it fails somebody? Pretty much at my wits end on this...any tips would be helpful!
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
try
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return View();
}
// Sign in the user with this external login provider if the user already has a login
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
case SignInStatus.Failure:
// this code attempts to create a user if it doesn't exist.
// it runs once for everybody except user in question
// problem user goes here every time, even though they have info in ASPNetUsers
if (loginInfo.Login.LoginProvider == "Windows")
{
bool resultMgr = await ManageWindowsLogins(loginInfo);
if (resultMgr) return RedirectToAction("Index", "Home");
else return RedirectToAction("NoLogin", "Account");
}
// Code should never go here
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}
catch (Exception ex)
{
// code does not normally go here
...log error message code...
return View();
}
}
Is there a better way of checking whether a user is logged in? Because I use the following approach for multiple apps and serving them somehow causes disparities, since it confuses the current app's item with other app's items.
I check whether a user is logged in like this:
constructor(private afAuth: AngularFireAuth, private router: Router, private db: AngularFirestore) {
this.userData = new ReplaySubject<UserDetails>();
afAuth.auth.onAuthStateChanged(user => {
if (user) {
this.user = user;
const local = localStorage.getItem('user');
if (local !== null) {
this.userData.next(JSON.parse(localStorage.getItem('user')));
} else {
this.fetchUserData();
}
} else {
localStorage.setItem('user', null);
}
});
}
get isLoggedIn(): boolean {
const user = localStorage.getItem('user');
return user !== 'null';
}
If each app is served from its own domain, then each will have its own localStorage and there can't be any conflict/confusion between them.
If you're serving multiple apps from the same domain, you'll have to use a unique name in the local storage for each app. Something like localStorage.setItem('app1_user', null) vs localStorage.setItem('app2_user', null).
But note that Firebase Authentication only has a single authenticated user per domain. So if you're serving multiple apps from the same domain, the user is (according to Firebase Authentication) signed in to all of them (or to none of them) at the same time.
Is it possible to obtain an OAuth2 refresh token of a user that has previously provided authorization, without having to ask for authorization again?
Context:
I have a Google Apps Script add-on that has obtained authorization for a number of scopes (including the ability to run when they are not present - see below screenshot). I am extending the project to include a Google App Engine web application that requires access to the same scopes (no additional scopes required). I am hoping to utilize the existing scopes that have previously been granted without having to request them again.
Ability to run when they are not present screenshot:
Any guidance on whether it is possible and the best approach is appreciated.
In order to get a Refresh token you must request offline access when the user is authenticating this way the user is asked if they are willing for your application to have off line access to their data. Think of it as an extra scope of permissions.
If you need a refresh token and didnt obtain it at the time you authenticated your user then your going to have to request authentication again.
You can use a time trigger to refresh the token every hour and then save this token into your database (make sure the user had authorized your script to create the trigger)
Whenever you need to do something that needs a user's authorization, you just need to use this token without asking the user
For example:
export const generateOAuthToken = () => {
try {
const user = Session.getEffectiveUser()
let accessToken
let error = ''
try {
accessToken = ScriptApp.getOAuthToken()
} catch (err) {
error = err.message
}
// Connect to database
const db = connectDatabase()
if (error === '') {
// Generate new token and save to database
// ... here
} else {
// Save the error to display to the user
}
} catch (err) {
console.error(err)
}
}
export const setupOAuthTrigger = () => {
const form = FormApp.getActiveForm()
const triggers = ScriptApp.getUserTriggers(form)
// Create a new trigger if required; delete existing trigger if it is not needed.
let existingTrigger = null
for (let i = 0; i < triggers.length; i += 1) {
if (triggers[i].getEventType() === ScriptApp.EventType.CLOCK) {
existingTrigger = triggers[i]
break
}
}
// TODO: Optimize this later, at the moment it's for cleaning up trigger in case of error
if (existingTrigger) {
ScriptApp.deleteTrigger(existingTrigger)
}
// Trigger every hour
ScriptApp.newTrigger('generateOAuthToken')
.timeBased()
.everyHours(1)
.create()
}
I have a Chrome extension that requests a user to login using the chrome.identity.getAuthToken route. This works fine, but when you login you can only use the users that you have accounts in Chrome for.
The client would like to be able to login with a different Google account; so rather than using the.client#gmail.com, which is the account Chrome is signed in to, they want to be able to login using the.client#company.com, which is also a valid Google account.
It is possible for me to be logged in to Chrome with one account, and Gmail with a second account, and I do not get the option to choose in the extension.
Is this possible?
Instead of authenticating the user using the chrome.identity.getAuthToken , just implement the OAuth part yourself.
You can use libraries to help you, but the last time I tried the most helpful library (the Google API Client) will not work on a Chrome extension.
Check out the Google OpenID Connect documentation for more info. In the end all you have to do is redirect the user to the OAuth URL, use your extension to get Google's answer (the authorization code) and then convert the authorization code to an access token (it's a simple POST call).
Since for a Chrome extension you cannot redirect to a web server, you can use the installed app redirect URI : urn:ietf:wg:oauth:2.0:oob. With this Google will display a page containing the authorization code.
Just use your extension to inject some javascript code in this page to get the authorization code, close the HTML page, perform the POST call to obtain the user's email.
Based on David's answer, I found out that chrome.identity (as well as generic browser.identity) API now provides a chrome.identity.launchWebAuthFlow method which can be used to launch an OAuth workflow. Following is a sample class showing how to use it:
class OAuth {
constructor(clientId) {
this.tokens = [];
this.redirectUrl = chrome.identity.getRedirectURL();
this.clientId = clientId;
this.scopes = [
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/gmail.send"
];
this.validationBaseUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo";
}
generateAuthUrl(email) {
const params = {
client_id: this.clientId,
response_type: 'token',
redirect_uri: encodeURIComponent(this.redirectUrl),
scope: encodeURIComponent(this.scopes.join(' ')),
login_hint: email
};
let url = 'https://accounts.google.com/o/oauth2/auth?';
for (const p in params) {
url += `${p}=${params[p]}&`;
}
return url;
}
extractAccessToken(redirectUri) {
let m = redirectUri.match(/[#?](.*)/);
if (!m || m.length < 1)
return null;
let params = new URLSearchParams(m[1].split("#")[0]);
return params.get("access_token");
}
/**
Validate the token contained in redirectURL.
This follows essentially the process here:
https://developers.google.com/identity/protocols/OAuth2UserAgent#tokeninfo-validation
- make a GET request to the validation URL, including the access token
- if the response is 200, and contains an "aud" property, and that property
matches the clientID, then the response is valid
- otherwise it is not valid
Note that the Google page talks about an "audience" property, but in fact
it seems to be "aud".
*/
validate(redirectURL) {
const accessToken = this.extractAccessToken(redirectURL);
if (!accessToken) {
throw "Authorization failure";
}
const validationURL = `${this.validationBaseUrl}?access_token=${accessToken}`;
const validationRequest = new Request(validationURL, {
method: "GET"
});
function checkResponse(response) {
return new Promise((resolve, reject) => {
if (response.status != 200) {
reject("Token validation error");
}
response.json().then((json) => {
if (json.aud && (json.aud === this.clientId)) {
resolve(accessToken);
} else {
reject("Token validation error");
}
});
});
}
return fetch(validationRequest).then(checkResponse.bind(this));
}
/**
Authenticate and authorize using browser.identity.launchWebAuthFlow().
If successful, this resolves with a redirectURL string that contains
an access token.
*/
authorize(email) {
const that = this;
return new Promise((resolve, reject) => {
chrome.identity.launchWebAuthFlow({
interactive: true,
url: that.generateAuthUrl(email)
}, function(responseUrl) {
resolve(responseUrl);
});
});
}
getAccessToken(email) {
if (!this.tokens[email]) {
const token = await this.authorize(email).then(this.validate.bind(this));
this.tokens[email] = token;
}
return this.tokens[email];
}
}
DISCLAIMER: above class is based on open-source sample code from Mozilla Developer Network.
Usage:
const clientId = "YOUR-CLIENT-ID"; // follow link below to see how to get client id
const oauth = new OAuth();
const token = await oauth.getAccessToken("sample#gmail.com");
Of course, you need to handle the expiration of tokens yourself i.e. when you get 401 from Google's API, remove token and try to authorize again.
A complete sample extension using Google's OAuth can be found here.
I am using connect-mysql-session to store sessions in db. Now my question is how do i add user data containing browser agent and ip-adress to check if the session is valid? How do i obtain that information? And how do i check if it matches?
users.login(credentials,function(err, results) {
//On errors
if (err) {
res.render(routes.index, {
title: 'Login'
});
//On success
} else if (results[0]) {
//Set session data and redirect to start page
req.session.userdata = results[0];
req.session.userdata.email = req.body.email_login;
req.session.is_logged_in = true;
res.redirect('/start');
//Wrong credentials
} else {
req.flash('warning','Wrong password or login');
res.render('index', {
title: 'Login'
});
}
});
Update:
I now got this added to the session:
req.session.ip = req.connection.remoteAddress;
req.session.useragent = req.headers['user-agent'];
and check for it in my auth middleware:
if(req.session.userdata && req.session.is_logged_in === true && req.session.ip === req.connection.remoteAddress && req.session.useragent === req.headers['user-agent']) {
next();
} else {
res.redirect('/');
}
Is this secure or do you see any risks with this? Should i go about it another way?
Your implementation looks good, and will give you some - very - basic protection against session hijacking.
However, I'm not sure I understand your middleware. What prevents an user from requesting /start directly? And, more importantly, as the middleware intercepts all requests, even those for /start, doesn't this look like some infinite redirect loop?
My suggestion would simply be to consider a user logged out at all time when ip or user agent mismatches.