1 | import { parseQuery } from "./util.js";
|
2 | import { ERR_HASS_HOST_REQUIRED, ERR_INVALID_AUTH, ERR_INVALID_HTTPS_TO_HTTP } from "./errors.js";
|
3 | export const genClientId = () => `${location.protocol}//${location.host}/`;
|
4 | export const genExpires = (expires_in) => {
|
5 | return expires_in * 1000 + Date.now();
|
6 | };
|
7 | function genRedirectUrl() {
|
8 |
|
9 | const { protocol, host, pathname, search } = location;
|
10 | return `${protocol}//${host}${pathname}${search}`;
|
11 | }
|
12 | function genAuthorizeUrl(hassUrl, clientId, redirectUrl, state) {
|
13 | let authorizeUrl = `${hassUrl}/auth/authorize?response_type=code&redirect_uri=${encodeURIComponent(redirectUrl)}`;
|
14 | if (clientId !== null) {
|
15 | authorizeUrl += `&client_id=${encodeURIComponent(clientId)}`;
|
16 | }
|
17 | if (state) {
|
18 | authorizeUrl += `&state=${encodeURIComponent(state)}`;
|
19 | }
|
20 | return authorizeUrl;
|
21 | }
|
22 | function redirectAuthorize(hassUrl, clientId, redirectUrl, state) {
|
23 |
|
24 | redirectUrl += (redirectUrl.includes("?") ? "&" : "?") + "auth_callback=1";
|
25 | document.location.href = genAuthorizeUrl(hassUrl, clientId, redirectUrl, state);
|
26 | }
|
27 | async function tokenRequest(hassUrl, clientId, data) {
|
28 |
|
29 |
|
30 |
|
31 | const l = typeof location !== "undefined" && location;
|
32 | if (l && l.protocol === "https:") {
|
33 |
|
34 | const a = document.createElement("a");
|
35 | a.href = hassUrl;
|
36 | if (a.protocol === "http:" && a.hostname !== "localhost") {
|
37 | throw ERR_INVALID_HTTPS_TO_HTTP;
|
38 | }
|
39 | }
|
40 | const formData = new FormData();
|
41 | if (clientId !== null) {
|
42 | formData.append("client_id", clientId);
|
43 | }
|
44 | Object.keys(data).forEach(key => {
|
45 | formData.append(key, data[key]);
|
46 | });
|
47 | const resp = await fetch(`${hassUrl}/auth/token`, {
|
48 | method: "POST",
|
49 | credentials: "same-origin",
|
50 | body: formData
|
51 | });
|
52 | if (!resp.ok) {
|
53 | throw resp.status === 400 ||
|
54 | resp.status === 403
|
55 | ? ERR_INVALID_AUTH
|
56 | : new Error("Unable to fetch tokens");
|
57 | }
|
58 | const tokens = await resp.json();
|
59 | tokens.hassUrl = hassUrl;
|
60 | tokens.clientId = clientId;
|
61 | tokens.expires = genExpires(tokens.expires_in);
|
62 | return tokens;
|
63 | }
|
64 | function fetchToken(hassUrl, clientId, code) {
|
65 | return tokenRequest(hassUrl, clientId, {
|
66 | code,
|
67 | grant_type: "authorization_code"
|
68 | });
|
69 | }
|
70 | function encodeOAuthState(state) {
|
71 | return btoa(JSON.stringify(state));
|
72 | }
|
73 | function decodeOAuthState(encoded) {
|
74 | return JSON.parse(atob(encoded));
|
75 | }
|
76 | export class Auth {
|
77 | constructor(data, saveTokens) {
|
78 | this.data = data;
|
79 | this._saveTokens = saveTokens;
|
80 | }
|
81 | get wsUrl() {
|
82 |
|
83 | return `ws${this.data.hassUrl.substr(4)}/api/websocket`;
|
84 | }
|
85 | get accessToken() {
|
86 | return this.data.access_token;
|
87 | }
|
88 | get expired() {
|
89 | return Date.now() > this.data.expires;
|
90 | }
|
91 | |
92 |
|
93 |
|
94 | async refreshAccessToken() {
|
95 | if (!this.data.refresh_token)
|
96 | throw new Error("No refresh_token");
|
97 | const data = await tokenRequest(this.data.hassUrl, this.data.clientId, {
|
98 | grant_type: "refresh_token",
|
99 | refresh_token: this.data.refresh_token
|
100 | });
|
101 |
|
102 | data.refresh_token = this.data.refresh_token;
|
103 | this.data = data;
|
104 | if (this._saveTokens)
|
105 | this._saveTokens(data);
|
106 | }
|
107 | |
108 |
|
109 |
|
110 | async revoke() {
|
111 | if (!this.data.refresh_token)
|
112 | throw new Error("No refresh_token to revoke");
|
113 | const formData = new FormData();
|
114 | formData.append("action", "revoke");
|
115 | formData.append("token", this.data.refresh_token);
|
116 |
|
117 | await fetch(`${this.data.hassUrl}/auth/token`, {
|
118 | method: "POST",
|
119 | credentials: "same-origin",
|
120 | body: formData
|
121 | });
|
122 | if (this._saveTokens) {
|
123 | this._saveTokens(null);
|
124 | }
|
125 | }
|
126 | }
|
127 | export function createLongLivedTokenAuth(hassUrl, access_token) {
|
128 | return new Auth({
|
129 | hassUrl,
|
130 | clientId: null,
|
131 | expires: Date.now() + 1e11,
|
132 | refresh_token: "",
|
133 | access_token,
|
134 | expires_in: 1e11
|
135 | });
|
136 | }
|
137 | export async function getAuth(options = {}) {
|
138 | let data;
|
139 | let hassUrl = options.hassUrl;
|
140 |
|
141 | if (hassUrl && hassUrl[hassUrl.length - 1] === "/") {
|
142 | hassUrl = hassUrl.substr(0, hassUrl.length - 1);
|
143 | }
|
144 | const clientId = options.clientId !== undefined ? options.clientId : genClientId();
|
145 |
|
146 | if (!data && options.authCode && hassUrl) {
|
147 | data = await fetchToken(hassUrl, clientId, options.authCode);
|
148 | if (options.saveTokens) {
|
149 | options.saveTokens(data);
|
150 | }
|
151 | }
|
152 |
|
153 | if (!data) {
|
154 | const query = parseQuery(location.search.substr(1));
|
155 |
|
156 | if ("auth_callback" in query) {
|
157 |
|
158 | const state = decodeOAuthState(query.state);
|
159 | data = await fetchToken(state.hassUrl, state.clientId, query.code);
|
160 | if (options.saveTokens) {
|
161 | options.saveTokens(data);
|
162 | }
|
163 | }
|
164 | }
|
165 |
|
166 | if (!data && options.loadTokens) {
|
167 | data = await options.loadTokens();
|
168 | }
|
169 | if (data) {
|
170 | return new Auth(data, options.saveTokens);
|
171 | }
|
172 | if (hassUrl === undefined) {
|
173 | throw ERR_HASS_HOST_REQUIRED;
|
174 | }
|
175 |
|
176 | redirectAuthorize(hassUrl, clientId, options.redirectUrl || genRedirectUrl(), encodeOAuthState({
|
177 | hassUrl,
|
178 | clientId
|
179 | }));
|
180 |
|
181 | return new Promise(() => { });
|
182 | }
|