diff --git a/providers/tesla/tesla-agent.ts b/providers/tesla/tesla-agent.ts index b916f6bf..58b8b06c 100644 --- a/providers/tesla/tesla-agent.ts +++ b/providers/tesla/tesla-agent.ts @@ -114,6 +114,7 @@ export class TeslaAgent extends AbstractAgent { // API Token check and update const token = job.serviceData.token as TeslaToken; if (TeslaAPI.tokenExpired(token)) { + log(LogLevel.Trace, `${job.serviceID} token expired, calling server API for refresh`); // Token has expired, run it through server const newToken = await this.scClient.providerMutate("tesla", { mutation: TeslaProviderMutates.RefreshToken, diff --git a/providers/tesla/tesla-api.ts b/providers/tesla/tesla-api.ts index a7edda48..1234cb0e 100644 --- a/providers/tesla/tesla-api.ts +++ b/providers/tesla/tesla-api.ts @@ -62,14 +62,17 @@ export class TeslaAPI { ): Promise { try { log(LogLevel.Trace, `authorize(${code}, ${callbackURI})`); - const authResponse = (await this.authAPI.post("/oauth2/v3/token", { - grant_type: "authorization_code", - client_id: config.TESLA_CLIENT_ID, - client_secret: config.TESLA_CLIENT_SECRET, - code: code, - audience: config.TESLA_API_BASE_URL, - redirect_uri: callbackURI, - })) as any; + + // Tesla authAPI expects form data in the body + const formData = new URLSearchParams(); + formData.append("grant_type", "authorization_code"); + formData.append("client_id", config.TESLA_CLIENT_ID); + formData.append("client_secret", config.TESLA_CLIENT_SECRET); + formData.append("code", code); + formData.append("audience", config.TESLA_API_BASE_URL); + formData.append("redirect_uri", callbackURI); + + const authResponse = (await this.authAPI.post("/oauth2/v3/token", formData.toString())) as any; return this.parseTokenResponse(authResponse); } catch (e) { console.debug(`TeslaAPI.authorize error: ${e}`); @@ -81,14 +84,13 @@ export class TeslaAPI { try { log(LogLevel.Trace, `renewToken(${refresh_token})`); - // Tesla authentication is multi layered at the moment - // First you need to renew the new Tesla SSO access token by using - // the refresh token from previous oauth2/v3 authorization - const authResponse = (await this.authAPI.post("/oauth2/v3/token", { - grant_type: "refresh_token", - client_id: config.TESLA_CLIENT_ID, - refresh_token: refresh_token, - })) as any; + // Tesla authAPI expects form data in the body + const formData = new URLSearchParams(); + formData.append("grant_type", "refresh_token"); + formData.append("client_id", config.TESLA_CLIENT_ID); + formData.append("refresh_token", refresh_token); + + const authResponse = (await this.authAPI.post("/oauth2/v3/token", formData.toString())) as any; return this.parseTokenResponse(authResponse); } catch (e) { console.debug(`TeslaAPI.renewToken error: ${e}`); @@ -231,6 +233,7 @@ const authClient = new RestClient({ headers: { Accept: "*/*", "User-Agent": "smartcharge.dev/1.0", + "Content-Type": "application/x-www-form-urlencoded", } }); diff --git a/providers/tesla/tesla-server.ts b/providers/tesla/tesla-server.ts index 8b283fbf..189c5d40 100644 --- a/providers/tesla/tesla-server.ts +++ b/providers/tesla/tesla-server.ts @@ -43,14 +43,20 @@ export async function maintainToken( token.expires_at !== undefined && !TeslaAPI.tokenExpired(token as TeslaToken) ) { + log(LogLevel.Trace, `Token ${token.access_token} is still valid`); return token as TeslaToken; } + log(LogLevel.Trace, `Token ${token.access_token} is invalid, calling renewToken`); const newToken = await teslaAPI.renewToken(token.refresh_token); validToken(db, token.refresh_token, newToken); return newToken; - } catch (err) { - log(LogLevel.Error, err); - invalidToken(db, token); + } catch (err:any) { + if (err && err.message === "login_required") { + log(LogLevel.Warning, `Token ${token.refresh_token} is invalid (login_required)`); + invalidToken(db, token); + } else { + log(LogLevel.Error, `Unexpected error raised when renewing token ${JSON.stringify(err)}`); + } throw new ApolloError("Invalid token", "INVALID_TOKEN"); } } @@ -205,6 +211,7 @@ const server: IProviderServer = { return await authorize(context.db, data.code, data.callbackURI); } case TeslaProviderMutates.RefreshToken: { + log(LogLevel.Trace, `TeslaProviderMutates.RefreshToken ${JSON.stringify(data)}`); return await maintainToken( context.db, data.token diff --git a/shared/restclient.ts b/shared/restclient.ts index 797c8955..50b04513 100644 --- a/shared/restclient.ts +++ b/shared/restclient.ts @@ -71,7 +71,7 @@ export class RestClient { method: string, relativeURL: string, data: any, - bearerToken?: string + bearerToken?: string, ): Promise { const url = mergeURL(this.options.baseURL, relativeURL); const secure = /^https:/.test(url); @@ -81,10 +81,10 @@ export class RestClient { method: method, timeout: this.options.timeout, headers: { - ...this.options.headers, Accept: "application/json", "Content-Type": "application/json", - "Accept-Encoding": "gzip, deflate" + "Accept-Encoding": "gzip, deflate", + ...this.options.headers, }, }; if (opt.headers["User-Agent"] === undefined) { @@ -97,15 +97,14 @@ export class RestClient { : "Authorization" ] = `Bearer ${bearerToken}`; } - const postData = JSON.stringify(data); + // Check if data is a string, if not assume it is JSON + const postData = typeof data === "string" ? data : JSON.stringify(data); if (postData) { opt.headers["Content-Length"] = Buffer.byteLength(postData); } return new Promise((resolve, reject) => { const dispatchError = (e: any, code?: number) => { - const s = `request error: ${code ? code + " " : ""}${ - typeof e === "string" ? e : JSON.stringify(e) - }`; + const s = (typeof e === "string" ? e : `${code ? code + " " : ""}${JSON.stringify(e)}`); reject(new RestClientError(s, code || 500)); }; @@ -144,7 +143,14 @@ export class RestClient { } else { console.log(`RestClientError: Non-2xx status: ${res.statusCode}`); console.log(`RestClientError: ${body}`); - dispatchError(res.statusMessage, res.statusCode); + try { + const data = JSON.parse(body); + const message = data.message || data.error || JSON.stringify(data); + console.log(`RestClientError: ${message}`); + dispatchError(message, res.statusCode); + } catch (e: any) { + dispatchError(res.statusMessage, res.statusCode); + } } }); });