your DB… implementing reliable login (encryption, etc.) is hard! • Avoids "yet another password to remember" problem • Many websites (Facebook, Google) have more secure login mechanisms than can be hand- rolled: 2-factor authentication, IP geolocation, etc… • Requires the user to already have (or to create) a FB, Google, etc. account • If 3rd-party website’s passwords get hacked, you got hacked • What happens if someone closes their Facebook account? • TLDR; must weigh potential business use cases for OAuth vs. Native Login PROS CONS
account with your OAuth provider website of choice (Facebook, Google, etc.) — we will use Slack as the running example for this demo • After registering, click on "create a new app/ project" or something similar • This will give you three key pieces of information: Client (or Consumer/App) ID Client (or Consumer/App) Secret - Never store the Client Secret in frontend JS code, this should be stored on the backend Redirect (or Website) URL - Something you define explicitly, which they will redirect back to on successful login api.slack.com
href> tag disguised as a button, with the following URL parameters: scope: the type of data you want to fetch client_id: gotten from Step 1 state: an (optional) random string to prevent man-in-the-middle attacks (e.g. CRSF) redirect_uri: the path back to your app to process their response, must be URL escaped (e.g. space = '%20') • Style the button as you wish (e.g. "Sign in with Slack"); there are also button generators out there • When the user clicks on this button, it redirects them to Slack’s OAuth Portal <a href="https://slack.com/oauth/authorize ?scope=identity.basic%20identity.email%20identity.avatar &client_id=<your_client_id> &state=<some_optional_random_hash> &redirect_uri=https%3A%2F%2Fmycoolapp.com%2Flogin "> Your App Slack slack.com/oauth/authorize Slack OAuth Portal slack.com/oath/authorize
and allows Slack to integrate with your app, Slack will send the request back to your redirect URL with the following URL parameters: code: a temporary code (which expires in 10 mins) to be swapped for a more permanent access_token state: the same random string you submitted before, check that the two strings match to prevent middleman attacks • * Pro Tip: The state param can contain whatever you’d like, not just a random string. Sometimes it’s useful to rig this piece of info with something else — a JWT, a secondary redirect_url… Just saying mycoolapp.com/login? ?code=abc123 &state=<same_random_hash_that_was_sent> Your App Slack mycoolapp.com/login
piece of data, the code, send this back to Slack along with the client_id and client_secret via your backend and not your frontend << very important. Whatever backend you use (Node.js, .NET, Java, Ruby, Python, Go) use the backend to submit a request with those URL params and return the resulting JSON response to the frontend. • While it’s possible, with some CORS sidesteps, to wire everything up via the frontend-only, it is highly not recommended, because you cannot hide your client_secret effectively on client side browser code — even using Webpack minify/ uglify, gzipping, Vue CLI, and other mangling techniques. slack.com/api/oauth.access? ?code=abc123 &client_id=<your_client_id> &client_secret=<your_client_secret> Your App’s Backend Slack slack.com/api/oauth.access // Login.vue export default { name: 'Login', computed: { code() { return this.$route.query.code; }, state() { return this.$route.query.state; }, }, created() { this.submitLogin(); }, methods: { async submitLogin() { const { data } = await axios.post('http://api.mycoolapp.com', { code: this.code, state: this.state } ); console.log(data); /* { "name": "John Tran", "email": "tran.spam@yahoo.com", "avatar": " ", "access_token": "xyz789" } */ // Hooray! Now what? breakpoint }, }, }; * Secrets are only truly secret on the server, not the browser Your App // backend.py import env, curl @api_route('http://api.mycoolapp.com') def fetch_slack_oauth_data(request): resp = curl.get(f’ https://slack.com/api/oauth.access ?code={request.params['code']} &client_id={env.CLIENT_ID} &client_secret={env.CLIENT_SECRET} ') if resp.ok: return Response(resp.json(), 200) return Response(resp.reason, 500) Python Example
grepping the resulting JS bundle and discover that "secret" or "my_p@55w0rd" is stored in plain-text. Not so secret. TLDR; Don’t store secrets on the frontend, like VUE_APP_SECRET. Use your backend’s ecosystem (DB, Vault, etc.) to manage secrets
user info in browser (e.g. cookie/local storage) so user can reuse session without logging in again (if they close tab, navigate elsewhere) • Save info to DB if app manages additional user- specific data other than name, email, avatar (e.g. roles/permissions, last login time) • Utilize the access_token granted to you to fetch additional app-specific data (e.g. Facebook friends, GitHub stars) if you requested additional scopes - The access_token serves as the user’s identity in lieu of their username/password // Store.vue export default new Vuex.Store({ state: { user: { name: '', email: '', avatar: '', token: '' }, }, mutations: { logIn(state, data) { state.user.name = data.name; state.user.email = data.email; state.user.avatar = data.avatar; state.user.token = data.access_token; }, actions: { async submitLogin(context, payload) { const { data } = await axios.post('http://api.mycoolapp.com', { code: payload.code, state: payload.state } ); context.commit('logIn', data); }, }, }); Your App’s Backend Slack api.mycoolapp.com { "name": "John Tran", "email": "tran.spam@yahoo.com", "avatar": " ", "access_token": "xyz789" } Your App // Login.vue export default { name: 'Login', computed: { code() { return this.$route.query.code; }, state() { return this.$route.query.state; }, }, created() { this.$store.dispatch('submitLogin', { code: this.code, state: this.state } ); // We are now logged in! this.$router.push('/home'); }, };
to configure HTTPS on your localhost - Typically done by configuring an NGINX SSL reverse proxy - Must create a self-signed certificate (letsencrypt.com, openssl.com) - Talk with backend or devops engineer if this is unfamiliar territory • Long Answer: Many providers (Google, Facebook) provide a <script /> tag to embed in the HTML directly, and by registering several callback functions via client-side code, you can get OAuth up and running. However, many of them also require the redirect_url to point to an https:// domain — simply using Webpack devServer 127.0.0.1:5000 won’t work ☹. P.S. I also don’t know how they hide their client_secret in the <script /> libraries, that’s their secret sauce. *Note: Slack doesn’t require https, but also doesn’t provide the simpler <script /> tag methodology.
{ ... methods: { onSignIn(googleUser) { const profileInfo = googleUser .getBasicProfile(); const authResp = gooleUser .getAuthResponse(); const data = { googleId: profileInfo.getId(), name: profileInfo.getName(), avatar: profileInfo.getImageUrl(), email: profileInfo.getEmail(), token: authResp.id_token, }; this.$store.commit('logIn', data); this.$router.push('/home'); }, async signOut() { const authInstance = gapi.auth2 .getAuthInstance(); await authInstance.signOut(); this.$store.commit('logOut'); }, }, ... }, Configure OAuth Client-Side • We’ll use Google as the running example.* • *Add Google’s script tag to base HTML template (public/index.html): <script src="https://apis.google.com/ js/platform.js" async defer></script> - This script will inject some global variables (gapi, g-signin2, etc.) into your code • Add a meta tag to the <head> of your HTML template that declares your Client ID: <meta name="google-signin-client_id" content="<client_id>.apps.googleuserc ontent.com"> • In your Vue login page (e.g. Login.vue) add some pre-built Google buttons for Login/Logout: <div class="g-signin2" data- onsuccess="onSignIn" /> <button @click="signOut">Log Out</ button> * Facebook’s is slightly more complicate — same idea, just more callbacks