Skip to content

Commit

Permalink
setting up and oauth flow
Browse files Browse the repository at this point in the history
  • Loading branch information
Fillipe Massuda committed Mar 13, 2021
1 parent 2f722c3 commit 42a5d64
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 87 deletions.
1 change: 1 addition & 0 deletions .env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VUE_APP_CLIENT_ID=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules
# local env files
.env.local
.env.*.local
.env

# Log files
npm-debug.log*
Expand Down
51 changes: 48 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,52 @@
# thinkific-oauth-pkce-vuejs-example
# Thinkific OAuth VueJS Starter

This is an example of how to setup a Vue.JS application to request access_token from a Thinkific Site.

## Prerequisites

You will need the credentials for your app created in Thinkific Platform.

- client_id: App's client id;
- redirect_uri: App's registered callback uri. For this project will be your app's url + `/callback`.

## Description

The project uses OAuth authorization code flow with Proof of Key for Code Exchange (PKCE) to retrieve the `access_token`.

This flow is recommended when storing app's `client_secret` is not safe.

The application generates a cryptographically random string called `code_verifier`.
The `code_challenge` is created from the `code_verifier` using `SHA256` and converting to base64 urlsafe.

First the `Authorize` link will redirect the user to the Thinkific Site with:

- client_id: app's client_id
- response_type: `code` (informs that the flow to exchange the access_token will be authorization_code flow)
- redirect_uri: app's url + `/callback`
- code_challenge: `base64urlsafe(sha256(codeVerifier))`
- code_challenge_method: `S256` (method used to generate the challenge)
- state: state parameter - *optional*

Once the user gives their consent to the application, Thinkific will redirect the authorization `code` to the `redirect_uri`, in our case, the `/callback` route.

The application then retrieves the code and executes the token request sending the `code_verifier`, `code` and `grant_type`.
Thinkific validates the `code` and `code_verifier` and respond with the `access_token` which then the application can use to invoke Thinkific's API.

## Project setup

Create `.env` file

```
cp .env-example .env
```

and populate the `VUE_APP_CLIENT_ID` with your app's client_id

```
VUE_APP_CLIENT_ID=
```

Install dependencies:
```
yarn install
```
Expand All @@ -20,5 +66,4 @@ yarn build
yarn lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"name": "thinkific-oauth-pkce-vuejs-example",
"version": "0.1.0",
"name": "thinkific-oauth-vuejs-starter",
"description": "A starter application with OAuth authorization",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0"
Expand Down
5 changes: 2 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</template>
Expand All @@ -21,10 +20,10 @@
#nav a {
font-weight: bold;
color: #2c3e50;
color: #5668c7;
}
#nav a.router-link-exact-active {
color: #42b983;
color: #100d59;
}
</style>
Binary file removed src/assets/logo.png
Binary file not shown.
59 changes: 0 additions & 59 deletions src/components/HelloWorld.vue

This file was deleted.

31 changes: 31 additions & 0 deletions src/components/Token.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div class="wrapper">
<div v-if="accessToken">
Access Token: {{ accessToken }}
</div>
<div v-if="expiresAt">
Expires at: {{ expirationTime }}
</div>
</div>
</template>
<script>
export default {
name: 'Token',
props: {
accessToken: {},
expiresAt: {},
},
computed: {
expirationTime() {
return new Date(parseInt(this.expiresAt, 10)).toString();
},
},
};
</script>
<style scoped>
.wrapper {
padding: 20px;
}
</style>
14 changes: 6 additions & 8 deletions src/router/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createRouter, createWebHashHistory } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import Callback from '../views/Callback.vue';

const routes = [
{
Expand All @@ -8,17 +9,14 @@ const routes = [
component: Home,
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
path: '/callback',
name: 'Callback',
component: Callback,
},
];

const router = createRouter({
history: createWebHashHistory(),
history: createWebHistory(),
routes,
});

Expand Down
5 changes: 0 additions & 5 deletions src/views/About.vue

This file was deleted.

56 changes: 56 additions & 0 deletions src/views/Callback.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<div />
</template>

<script>
import axios from 'axios';
export default {
name: 'Callback',
async created() {
// receives callback to request token
// code and subdomain are querystring parameters
// another field is `state` that if sent in the authorize request
// will be returned back useful if you need to send state data between
// authorize and callback and also useful to prevent possible csrf attacks
const { code, subdomain } = this.$route.query;
const tokenUrl = `https://${subdomain}.thinkific.com/oauth2/token`;
const tokenResponse = await axios.post(tokenUrl, {
grant_type: 'authorization_code',
code_verifier: localStorage.getItem('codeVerifier'),
code,
}, {
auth: {
username: process.env.VUE_APP_CLIENT_ID,
},
});
// Token response attributes:
// {
// access_token: <access_token>,
// expires_in: <seconds to expire>,
// gid: <global identifier for site>,
// token_type: "bearer"
// }
// store token
localStorage.setItem('accessToken', tokenResponse.data.access_token);
// store token expiration time
const expiresAt = this.calculateExpirationTime(tokenResponse.data.expires_in);
localStorage.setItem('expiresAt', expiresAt);
// redirect to home
this.$router.push('/');
},
methods: {
// Stores token expiration time
calculateExpirationTime(expiresIn) {
const now = new Date().getTime();
// sum secods to token generated time
return now + (expiresIn * 1000);
},
},
};
</script>
Loading

0 comments on commit 42a5d64

Please sign in to comment.