After two months of experimenting with Teams Authentication via adal.js and msal.js and failure, I’m close to giving up. So I really need your help.
Basically I need to “silently” authenticate the logged in Teams User for my own website (tab) inside my app that I created with App Studio. The reason for that is, so that I can use the data of the authentication token for the login of my own website.
So far I was only able to get this working with msal.js and a popup, which according to Teams developer I’ve asked is not the way to go. Understandable, since I cannot use the popup method on the Teams Client because it gets blocked.
I’ve tried this silent login method (https://github.com/OfficeDev/microsoft-teams-sample-complete-node/blob/master/src/views/tab-auth/silent.hbs) that was recommend to me.
Sadly it didn’t work. All I get is a “Renewal failed: Token renewal operation failed due to timeout” error.
Since the msal.js popup variant (Node.js Azure Quick Start Example) I used before worked in a web browser, I don’t think that the configuration of Azure App is wrong.
This is my code so far:
// onLoad="prepareForm()"
<!--- Import package for authentication information in Teams/Azure--->
<script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.15/js/adal.min.js" integrity="sha384-lIk8T3uMxKqXQVVfFbiw0K/Nq+kt1P3NtGt/pNexiDby2rKU6xnDY8p16gIwKqgI" crossorigin="anonymous"></script>
<script src="https://statics.teams.microsoft.com/sdk/v1.4.2/js/MicrosoftTeams.min.js" crossorigin="anonymous"></script>
<script language="JavaScript">
let config = {
clientId: "1402f497-d6e8-6740-9412-e12def41c451", // I've changed it for this stackoverflow post
redirectUri: "https://myredirect.com", // I've changed it for this stackoverflow post
cacheLocation: "localStorage",
navigateToLoginRequestUrl: false,
};
microsoftTeams.initialize()
/// START Functions for Teams
function getTeamsContext() {
microsoftTeams.getContext(function(context) {
startAuthentication(context);
});
}
function startAuthentication(teamsContext) {
if (teamsContext.loginHint) {
config.extraQueryParameters = "scope=openid+profile&login_hint=" + encodeURIComponent(teamsContext.loginHint);
} else {
config.extraQueryParameters = "scope=openid+profile";
}
let authContext = new AuthenticationContext(config);
user = authContext.getCachedUser();
if (user) {
if (user.profile.oid !== teamsContext.userObjectId) {
authContext.clearCache();
}
}
let token = authContext.getCachedToken(config.clientId);
if (token) {
console.log(token)
// Get content of token
} else {
// No token, or token is expired
authContext._renewIdToken(function (err, idToken) {
if (err) {
console.log("Renewal failed: " + err);
// Some way of logging in via Popup or similiar
} else {
console.log(idToken)
// Get content of token
}
});
}
}
/// END Functions for Teams
// initialized on page load!
function prepareForm() {
getTeamsContext();
document.InputForm.password.focus()
}
<script/>
Those are my questions:
What causes this error?
How do I authenticate the token on manipulation and is it Teams or Azure? (Does adal.js any functions for this?)
How do I login if the silent authentication fails and popups are blocked? Is there a website for authentication provided by Teams that returns a token?
Are there any working examples of the silent authentication that are not from the official Microsoft website? (I don't understand them.)
Related
I am trying to enable login & Sign Up with Phone Number + OTP for a website (not Mobile) just like Firebase Auth offers.
I have looked up endless tutorials, almost all of which require AWS Amplify, which then requires knowing React/Angular/Vue (I'm not a front end developer). I followed tutorials like this one (https://techinscribed.com/passwordless-phone-number-authentication-using-aws-amplify-cognito/) and have created all the Lambda Functions, Cognito UserPools as stated in the tutorial. My roadblock is that it requires Amplify, and I just want to use vanilla JavaScript.
So I downloaded the AWS SDK builder with:
AWS.CognitoIdentity
AWS.CognitoIdentityServiceProvider
AWS.CognitoSync
I am using Zappa with Flask (serverless) to render HTML + JS to the user. I have everything else configured with API Gateway for the backend. All I need to do is authenticate users and generate sessions tokens for authenticated users, allowing access to their personal data, (like saved info, favorites, etc).
I am praying for someone to help me figure out how I can authenticate my users and generate the session/JWT tokens for my users. Any guidance would be appreciated.
AWS Amplify is just a wrapper around the core AWS services. The goal is to provide a boilerplate that takes care of the common access patterns. You don't have to use framework if you don't want to and can use the core services directly.
Before I point you to these low level APIs, it's worth noting that Amplify does have vanilla JS APIs as well. Refer the official docs here. You can handle authentication with only JS and not worry about any frameworks.
The docs for the Authentication module can be found here.
For reference, here are the scripts for Sign-up and login:
import { Auth } from 'aws-amplify';
async function signUp() {
try {
const user = await Auth.signUp({
username,
password,
attributes: {
email, // optional
phone_number, // optional - E.164 number convention
// other custom attributes
}
});
console.log({ user });
} catch (error) {
console.log('error signing up:', error);
}
}
async function SignIn() {
try {
const user = await Auth.signIn(username, password);
} catch (error) {
console.log('error signing in', error);
}
}
Cotter co-founder here.
We have a simple library that allows you to send OTP verification to users via SMS/WhatsApp with Vanilla Javascript.
Guide: Sending OTP with HTML + Vanilla JS.
Working Example: in CodeSandbox (you need to add your API_KEY_ID, which you can get from the dashboard).
1. Import the library
<!-- 1️⃣ Get Cotter SDK -->
<script
src="https://js.cotter.app/lib/cotter.js"
type="text/javascript"
></script>
2. Make a div with id="cotter-form-container" to contain the form
<div
id="cotter-form-container"
style="width: 300px; height: 300px;"
></div>
3. Show the form
<!-- 3️⃣ Initialize Cotter with some config -->
<script>
var cotter = new Cotter("<YOUR_API_KEY_ID>"); // 👈 Specify your API KEY ID here
cotter
.signInWithOTP()
.showPhoneForm() // to send OTP to using email use .showEmailForm()
.then(payload => console.log(payload))
.catch(err => console.log(err));
</script>
4. Get JWT Tokens
Check your console logs, you should get a response like this:
{
"token": {...},
"phone": "+12345678910",
"oauth_token": {
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6I...", // use this
"id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IlN...",
"refresh_token": "27322:UYO4pcA17i4sCIYD...",
"expires_in": 3600,
"token_type": "Bearer",
"auth_method": "OTP"
},
"user": {
"ID": "abcdefgh-abcd-abcd-abcd-f17786ed499e", // Cotter User ID
... // more user info
}
}
Use the oauth_token.access_token for your sessions, here's how you can validate the JWT token.
5. Customize form
To show buttons to send the OTP via both SMS and WhatsApp, go to Dashboard > Branding.
I am creating a React web app where the user sign in/up and other authentication related processes are being handled by AWS Cognito and the accompanying Javascript SDK.
My app has some 'public' routes/pages that everybody, signed in or not, can view, such as /documentation/ and /sign-in/. There also exist various private routes which you can only see when you are logged in, such as /my-documents/.
At the moment, I have a working sign in page, where a user is signed in with code very similar to use case #4 (Cognito Docs).
My question now is: as soon as a user goes to /my-documents/, how do I check whether the user is signed in and actually has the rights to see this page?
I am not using AWS Amplify for the authentication in my app. I only use the NPM package 'amazon-cognito-identity-js'.
This is the code I currently use to check if the session is valid, in other words if the user is successfully signed in. This however, seems like a cumbersome way to check such a simple status.
const isAuthenticated = () => {
const cognitoUser = userPool.getCurrentUser();
let isSessionValid = false;
if (cognitoUser) {
cognitoUser.getSession((err: Error, result: CognitoUserSession) => {
if (!err) {
isSessionValid = result.isValid();
}
});
}
return isSessionValid;
};
isSessionValid is returned before the callback in getSession is executed.
I'm building a small JS app for my Microsoft ToDo tasks and use the msal.js library to accommodate the authentication process.
This works perfectly fine, I get a popup, I authenticate my profile, the popup closes and my tasks appear on my screen.
But: It doesn't seem to remember that I authenticated before; Every time I run my webpack app and the page is booted it shows the popup and asks for authentication. Once I've authenticated and just refresh my page, it just shows me the tasks without showing the popup again. I haven't tried waiting for an hour but I think it has something to do with not properly refreshing my access token. I'm not that involved with the Outlook/Microsoft API that I can really figure out if I'm using it correctly.
In short: How can I authenticate once so that the next time I start my app the tasks are shown without having to authenticate again?
My init function
this.userAgentApplication = new Msal.UserAgentApplication(microsoftTasksClientId, null, function (errorDes, token, error, tokenType) {
// this callback is called after loginRedirect OR acquireTokenRedirect (not used for loginPopup/aquireTokenPopup)
console.log(token)
})
let user = this.userAgentApplication.getUser()
if (!user) {
const self = this
// this.userAgentApplication = new Msal.UserAgentApplication(microsoftTasksClientId)
this.userAgentApplication.loginPopup([`${this.apiRootUrl}Tasks.readwrite`]).then(function (token) {
self.idToken = token
user = self.userAgentApplication.getUser()
if (user) {
self.getSilentToken()
}
}, function (error) {
console.log(error)
})
} else {
this.getSilentToken()
}
And my getSilentToken function
const self = this
this.userAgentApplication.acquireTokenSilent([`${this.apiRootUrl}Tasks.readwrite`]).then(function (token) {
console.log('ATS promise resolved', token)
self.accessToken = token
self.getTasks()
}, function (err) {
console.log(err)
})
Please not that my code isn't refactored AT ALL! ;-)
What version of MSAL are you using?
There is a problem in 0.1.1 version that storage is 'sessionStorage' by default and can't be really changed. In that case your login is saved just for currently opened window and you will be forced to relogin even when opened new browser window.
You should use 'localStorage' to achieve what you want and pass it as a constructor parameter for UserAgentApplication.
Here is a fix in their repo for this:
https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/eba99927ce6c6d24943db90dfebc62b948355f19
I am fairly new to developing chrome extensions, more specifically to the user authentication part in chrome extensions. I am following User Identity example from Google Developer docs.
The example works perfectly fine. I was able to generate the client id for the chrome app, add the scope for API's in my case Gmail API. And finally get the Auth Token by adding the identitypermission in manifest.json as follows
"oauth2": {
"client_id": "MY CLIENT ID",
"scopes": [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.modify"
]
}
And my app.js is a content_script which has the following code.
chrome.identity.getAuthToken({ 'interactive': true }, function(token) {
/* With which I can use xhr requests to get data from Gmail API */
console.log('Access Token : '+token);
});
Now this token that I get gives me the result for the user with which I have logged into chrome. Meaning Let's say I have a UserA with email address user_a#gmail.com and I have used this log into the chrome browser.
Question
How do I get the associated accounts or the secondary accounts? For instance, let's say a User Blogs into Gmail from the chrome browser. Is it possible to access the Gmail API for that particular user who is currently logged in?
I have tried a couple of things here.
gapi.auth.authorize({
'client_id': CLIENT_ID,
'scope': SCOPES.join(' '),
'immediate': true
},
function(authResult){//do something});
In the above scenario, the client id and scopes are fetched from the manifest.json using chrome.runtime.getManifest();.
This method uses the client.js from google api's and makes use of gapi variable.
In this case, I get the access token for the user whom I generated the client id, not even the chrome application user.
Furthermore, When I open an incognito mode and access this plugin, still I get the same user's access token.
Additional Note
I tried the same gapi.auth.authorize() using a Web OAuth 2 Client Id. It works perfectly fine. I mean whenever this authorize is executed it fetches the current logged in user's data or it asks for a login where the user can log in and authenticate. How do I achieve the same thing in chrome extension? Kindly let me know if I am missing something here.
As of now, this is not possible using supported APIs in Google Chrome stable (Version 63). However, in the Dev channel and most likely with a future release, the following will be possible:
chrome.identity.getAccounts(function(accounts) {
// accounts is a list of accounts.
chrome.identity.getAuthToken({ 'interactive': true, 'account': accounts[0] }, function(token) {
/* With which i can use xhr requests to get data from gmail api */
console.log('Access Token : '+token);
});
});
See the documentation for getAccounts().
EDIT: Something that might work in the meantime is registering for the onSigninChanged event.
How I ended up handling is was this (summary):
In the page layer, on load, I send a message down the stack to the background layer.
I used launchWebAuthFlow() to https://accounts.google.com/o/oauth2/auth to get the access_token for the account.
I made an AJAX call to https://www.googleapis.com/oauth2/v4/token using the access_token to get a refresh token.
When a user changes which account they are using via the avatar button on the top-right, this process is triggered again, as it is initiated by onLoad for the page layer of the extension.
The things left out the description above are caching and error handling, which are super-important.
http://developer.streak.com/2014/10/how-to-use-gmail-api-in-chrome-extension.html
is the complete solution which I have recently implemented on background page to work with Gmail API.
The content script is calling popup window to authorize using generated URL and simple server endpoint to store the refresh token.
$.oauthpopup = function (options) {
options.windowName = options.windowName || 'ConnectWithOAuth'; // should not include space for IE
var left = (screen.width / 2) - (800 / 2);
var top = (screen.height / 2) - (500 / 1.4);
options.windowOptions = options.windowOptions || 'location=0,status=0,width=800,height=500,left=' + left + ',top=' + top;
options.callback = options.callback || function () {
window.location.reload();
};
var that = this;
debug('oauthpopup open separate _oauthWindow');
that._oauthWindow = window.open(options.path, options.windowName, options.windowOptions);
};
$.oauthpopup({
path: 'https://accounts.google.com/o/oauth2/auth?' +
'access_type=offline' +
'&approval_prompt=force' +
'&client_id=' + clientID +
'&redirect_uri=' + callBackUrl +
'&response_type=code' +
'&scope=https://mail.google.com/ email profile' +
'&state=' + login.timyoUUID +
'&user_id=' + login.user_email,
callback: function () {
// do callback stuff
},
});
callBackUrl is used to store refresh token on the server.
Here is the example how I set access token for every request to gapi
export function setTokenForGAPI(accessToken) {
return getGAPIClient()
.then(() => {
const isSameToken = (currentAccessToken === accessToken);
const noToken = ((accessToken === undefined) || (accessToken === ''));
if (isSameToken || noToken) return;
gapi.auth.setToken({
access_token: accessToken,
});
currentAccessToken = accessToken;
})
.catch(function (e) {
console.log('error in setTokenForGAPI', e);
});
}
export function getEmailsByThreadIds(accessToken, ids) {
return setTokenForGAPI(accessToken)
.then(groupedThreadDetailsRequests(ids))
.then(processEmailDetailsResponse);
}
I am authenticating through Cognito on client side browser using a developer authenticated identity. When my page loads (or is refreshed) I would like my application to remember the Identity for as long as the object is not expired (I think it lasts about an hour). However, I don't know how to retrieve the identity from Cognito without having to go through the developer authentication again.
Here is what the code does on page load:
var cognitoCredentials
$(document).ready(function() {
"use strict";
cognitoParams = {
IdentityPoolId: 'us-east-1:xxxxxxx'
};
cognitoCredentials = new AWS.CognitoIdentityCredentials(cognitoParams);
AWS.config.credentials = cognitoCredentials;
});
And after logging in through the developer authentication:
cognitoCredentials.params.IdentityId = output.identityId;
cognitoCredentials.params.Logins = {
'cognito-identity.amazonaws.com': output.token
};
cognitoCredentials.expired = true;
If I have already logged in, and then refresh the page, and try to log in again I get an error that I am trying to get an identity when I already have one
Error: Missing credentials in config(…) NotAuthorizedException: Missing credentials in config
"Access to Identity 'us-east-1:xxxxxxx' is forbidden."
However, I don't know how to access it. How do I retrieve the credentials so that when the page is refreshed, I can detect the previous identity given by Cognito?
Save at least accessKeyId, secretAccessKey, sessionToken in sessionStorage between pages. You can load these into AWS.config.credentials (after the AWS SDK has been loaded of course). It is much faster than waiting for Cognito to respond. Keep in mind, you'll have to manually refresh them with a token from one of the providers and this is only good until the temporary token expires (~1 hour).
var credKeys = [
'accessKeyId',
'secretAccessKey',
'sessionToken'
];
// After Cognito login
credKeys.forEach(function(key) {
sessionStorage.setItem(key, AWS.config.credentials[key]);
});
// After AWS SDK load
AWS.config.region = 'us-east-1'; // pick your region
credKeys.forEach(function(key) {
AWS.config.credentials[key] = sessionStorage.getItem(key);
});
// Now make your AWS calls to S3, DynamoDB, etc
The only way to get back to the same identity on page refresh would be to use the same token used to initialize that identity. You may want to refer to this question as the problems are similar (replacing the Facebook token with the OpenId Connect token from the developer authenticated identities flow).
To reiterate what that question says: the credentials in the SDK will not be persisted across pages, so you should cache the token to be reused.
I take a slightly different approach, that allows the SDK to refresh the credentials.
In short, I serialize the AssumeRoleWithWebIdentityRequest JSON object to session storage.
Here is an example using Angular, but concept applies in any JS app:
const AWS = require('aws-sdk/global');
import { STS } from 'aws-sdk';
import { environment } from '../../environments/environment';
const WEB_IDENT_CREDS_SS_KEY = 'ic.tmpAwsCreds';
// Handle tmp aws creds across page refreshes
const tmpCreds = sessionStorage.getItem(WEB_IDENT_CREDS_SS_KEY);
if (!!tmpCreds) {
AWS.config.credentials = new AWS.WebIdentityCredentials(JSON.parse(tmpCreds));
}
#Injectable({
providedIn: 'root'
})
export class AuthService {
...
async assumeAwsRoleFromWebIdent(fbUser: firebase.User) {
const token = await fbUser.getIdToken(false);
let p: STS.Types.AssumeRoleWithWebIdentityRequest = {
...environment.stsAssumeWebIdentConfig,
//environment.stsAssumeWebIdentConfig contains:
//DurationSeconds: 3600,
//RoleArn: 'arn:aws:iam::xxx:role/investmentclub-fbase-trust',
RoleSessionName: fbUser.uid + '#' + (+new Date()),
WebIdentityToken: token
};
// Store creds across page refresh, duno WTF `new AWS.WebIdentityCredentials(p)` don't have an option for this
AWS.config.credentials = new AWS.WebIdentityCredentials(p);
sessionStorage.setItem(WEB_IDENT_CREDS_SS_KEY, JSON.stringify(p));
}
removeAwsTempCreds() {
AWS.config.credentials = {};
sessionStorage.removeItem(WEB_IDENT_CREDS_SS_KEY);
}
...
Few things to note:
Upon login, I store the WebIdentityCredentials parameters as a JSON string in session cache.
You'll notice I check the browser session cache in global scope, to
handle page refreshes (sets creds before they can be used).
A tutorial with complete example can be found on my blog