ユーザエージェントフローでsalesforce認証する
ウェブアプリからSalesforceで認証する処理を学習しました。
バックエンドサーバを使用せずVueで作成したウェブアプリからSalesforceへ接続してみます。
OAuth2のImplicit Grantになるかと思います。
Salesforceの設定
接続アプリケーションの作成
Salesforce に接続アプリケーションを作成します。
コールバックURLを設定します。
コンシューマキーが必要なので控えておきます。
CORSの設定
Salesforce でCORSの設定をします。
httpsにしましたが、localhostを使用する時はhttpでもいいかもしれないです。
VUE
https 設定
vite.configを変更してhttpsに変更する。
証明書の作り方は省略します。
1
2
3
4
5
6
|
server: {
https: {
key: fs.readFileSync('./serverinstall_key.pem'),
cert: fs.readFileSync('./server.crt'),
}
}
|
コールバック
Salesforceの接続アプリケーションのコールバックに設定したURLをルーティングする。
AuthCallback.vueコンポーネントで処理するように設定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name : 'HelloWorld',
component: HelloWorld
},
{
path: '/authcallback',
name : 'AuthCallback',
component: AuthCallback
}
]
})
|
ログインページの作成
HelloWorld.vue ファイルにログインボタンとログアウトボタンを表示します。
ログインしていない場合はログインボタン、ログインしている場合はログアウトボタンを表示します。
ログアウトボタンはマウスカーソルがユーザアイコンにホバーした時に表示するようにします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
<script setup>
import { onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { userStore } from '@/stores/userStore'
const user = userStore()
const { isLoggedIn } = storeToRefs(user)
const formAction = ref("" +
import.meta.env.VITE_SF_URL
+ "/services/oauth2/authorize"
+ "?client_id=" + import.meta.env.VITE_SF_KEY
+ "&redirect_uri=" + import.meta.env.VITE_AUTH_CALLBACK
+ "&response_type=token")
onMounted(() => {
if (user.isLoggedIn) {
// ログイン済みの場合、トークンが有効か確認する
// チェック処理は非同期で行われる
user.checkToken()
}
})
function logout() {
user.logout()
}
</script>
<template>
<form :action="formAction" method="post">
<button v-show="!isLoggedIn"
type="submit"
class="button is-info"
>
ログイン
</button>
</form>
<button v-show="isLoggedIn"
class="button is-info is-rounded user-info">
<font-awesome-icon icon="fa-regular fa-user" />
<aside class="menu popover">
<p class="menu-label">
{{ user.displayName }}
</p>
<ul class="menu-list">
<li>
<button class="button is-info"
@click="logout">
ログアウト
</button>
</li>
</ul>
</aside>
</button>
</template>
<style scoped lang="scss">
.user-info {
width: 1px;
position: relative;
.popover {
position: absolute;
top: 40px;
left: 0;
color: black;
background-color: white;
transition: 1s;
opacity: 0;
visibility: hidden;
}
&:hover {
.popover {
opacity: 1;
visibility: visible;
transition: .5s;
}
}
}
</style>
|
コールバックページの作成
AuthCallback.vue ファイルにコールバック処理を書きます。
mount されたらURLのハッシュ以降を取得してログイン結果を取得します。
ログインに成功したら login を 失敗したら logout を呼びます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
<script setup>
import { onMounted, ref } from 'vue'
import { userStore } from '@/stores/userStore'
import { useRoute } from 'vue-router'
const user = userStore()
const route = useRoute()
const status = ref("")
onMounted(() => {
const hashElements = {}
let hasError = false
let hasToken = false
if (route.hash) {
route.hash.substring(1).split('&').forEach(e => {
const hashSet = e.split('=')
hashElements[hashSet[0]] = hashSet[1]
if (hashSet[0] === "error") {
status.value = hashSet[1]
}
hasError ||= (hashSet[0] === "error")
hasToken ||= (hashSet[0] === "access_token")
})
}
if (!hasToken && !hasError) {
if (route.query.error) {
status.value = route.query.error
hasError = true
}
if (route.params.error) {
status.value = route.params.error
hasError = true
}
}
if (hasError) {
user.logout()
status.value = "ログインに失敗しました。"
}
if (hasToken) {
status.value = "ログインしました。"
user.login(hashElements)
window.location.replace("/")
}
})
</script>
<template>
<div class="m-4 has-text-centered">
{{ status }}
<br>
<a href="/">Top</a>
</div>
</template>
<style scoped>
</style>
|
ログイン情報をstoreに保存
ログイン情報は store に保存することにします。
localstorageに保存することが最善であるかどうかは、ここでは考えないことにします。
以下の3つの変数と3つのfunctionをexportします。
変数
- displayName: ユーザ名。
- token: アクセストークン。
- isLoggedIn: ログイン。
function
- login: 認証情報を保存する。
- logout: 認証情報を破棄する。
- checkToken: 認証情報が有効かチェックする。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
import {computed} from 'vue';
import {defineStore} from 'pinia'
import {useLocalStorage} from "@vueuse/core"
import axios from "axios"
export const userStore = defineStore('user', () => {
const auth = useLocalStorage("auth", "")
const user = useLocalStorage("user", "")
const isLoggedIn = computed(() => {
if (auth.value) {
return true
}
return false
})
const token = computed(() => {
if (auth.value) {
const a = JSON.parse(auth.value)
return a.access_token
}
return null
})
const displayName = computed(() => {
if (user.value) {
const u = JSON.parse(user.value)
return u.display_name
}
return null
})
function save(a) {
auth.value = JSON.stringify(a)
}
function clear() {
auth.value = ""
user.value = ""
}
function print() {
console.log("saved auth in local storage %o", auth.value)
console.log("saved user in local storage %o", user.value)
}
function login(a) {
revoke()
save(a)
print()
}
function logout() {
revoke()
print()
}
function checkToken() {
if (auth.value) {
const a = JSON.parse(auth.value)
axios.get(
a.id,
{
params: {
access_token: a.access_token,
format: "json"
}
})
.then(function (response) {
console.log(response);
user.value = JSON.stringify(response.data)
})
.catch(function (error) {
console.log(error);
user.value = ""
logout()
})
}
}
function revoke() {
if (auth.value) {
const a = JSON.parse(auth.value)
axios.get(
a.instance_url + "/services/oauth2/revoke",
{
headers: {
"Content-Type" : "application/x-www-form-urlencoded"
},
params: {
token: a.access_token
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.then(function () {
// always executed
clear()
})
} else {
clear()
}
}
return {
//auth, user, // devtools で確認したいときはコメントアウトを外す
displayName,
token,
isLoggedIn,
login, logout, checkToken
}
})
|
画面遷移
-
HelloWorld.vue のログインボタン
-
Salesforce のユーザ入力画面
-
Salesforce の認証画面
-
HelloWorld.vue のユーザ表示
-
HelloWorld,vue のログアウトボタン
その他のファイル
環境情報はenvファイルに記載しています。
1
2
3
4
|
NODE_ENV='local'
VITE_SF_URL="salesforceの認証URL"
VITE_SF_KEY="salesforce接続アプリケーションのコンシューマ鍵"
VITE_AUTH_CALLBACK="https://localhost:5173/authcallback"
|
スタイルはbulma、アイコンはfontawesomeを使用しています。
package.json
1
2
3
4
5
6
7
8
9
10
11
12
|
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/vue-fontawesome": "^3.0.2",
"@vueuse/core": "^9.5.0",
"@vueuse/router": "^9.5.0",
"axios": "^1.2.0",
"bulma": "^0.9.3",
"pinia": "^2.0.24",
"vue": "^3.2.41",
"vue-router": "^4.0.16"
},
|
気になったこと
トークンの有効時間
Salesforceから受け取る情報の中にアクセストークンの有効期間が含まれていませんでした。
セッションの有効期間と同じと思われます。下記のような処理を考えておいた方がよさそうです。
- ウェブアプリ側で、トークン取得から一定時間経過後にトークンを再取得する。
- ステータスコード401の場合に、再認証を促すようにする。