Smart things redirection end-point fails for google oauth

Hi guys,
I am trying to build a virtual device switch with the google oauth2.0 for the smarthings schema connector. However after the successful google authentication I am unable to redirect to the smartthings application. This is the redirection function of my code.
//Smarthings redirect
app.get(“/redirect-to-smartthings”, (req, res) => {
const clientId = process.env.ST_CLIENT_ID; // Your SmartThings client ID
const redirectUri = encodeURIComponent(“www.domain_name.ngrok-free.app/smartthings/callback”); // server’s redirect URI

const smartThingsAuthUrl = https://api.smartthings.com/oauth/authorize?client_id=${clientId}&response_type=code&scope=r:devices:*&redirect_uri=${redirectUri};
res.redirect(smartThingsAuthUrl);
});

app.get(
“/auth/google/callback”,
passport.authenticate(“google”, { failureRedirect: “/login” }),
function (req, res) {
res.redirect(‘/redirect-to-smartthings’);
}
);
I am getting an error on the api.smarthings.com page “unauthorized client” I don’t understand where is the issue exactly, kindly help

Hi @Rahul_Banerjee

I think the issue with smartThingsAuthUrl, when the flow starts SmartThings will send you a request to the Authorization URI with the following data:

  • Response Type
  • Client ID
  • Redirect URI
  • Scope
  • State

After that, the user authenticates the resource you need to redirect the user-agent back to the client, (this is maybe confusing to visualize here is a diagram), the redirection URI must be included an authorization code and the state that was provided by SmartThings earlier.

Something like:

https://example-app.com/cb?code=Yzk5ZDczMzRlNDEwY&state=5ca75bd30

Here is a good step-by-step Authorization Code Grant example

Hey Alejandro,
This is what my understanding is
" 1. User Authorization Request: Smartthings application redirects the user to the SmartThings authorization endpoint with the required query parameters (client_id, redirect_uri, response_type=code, scope, and state).
2. User Authenticates and Authorizes: The user logs in to SmartThings (if not already logged in) and authorizes my application for the requested scopes.
3. SmartThings Redirects Back to Your Application: SmartThings redirects the user back to my application using the redirect_uri provided earlier, appending an authorization code and the original state parameter to the URI as query parameters.
4. Your Application Receives the Code and State: Your application must validate the state parameter to ensure it matches the one i generated and stored before redirecting the user to SmartThings.
5. Exchange the Authorization Code for an Access Token: Once the state is validated, your application uses the received authorization code to request an access token from SmartThings’s token endpoint.
Is it correct, if so then I should generate the state request right?

I am simply trying to build a virtual swithc using a google oauth2.0, where the user will be prompted to login with oauth2.0 and then he/she will be redirected into the smartthings app, this is the code

///Smarthing redirect
app.get(“/redirect-to-smartthings”, (req, res) => {
const clientId = process.env.ST_CLIENT_ID;
const redirectUri = encodeURIComponent(“https://example.com.ngrok-free.app/smartthings/callback”); // server’s redirect URI
const state = generateStateParameter();
// Save the state in the user’s session or a similar mechanism to validate it later
req.session.oauthState = state;

const smartThingsAuthUrl = `https://api.smartthings.com/oauth/authorize?client_id=${clientId}&response_type=code&scope=r:devices:*&redirect_uri=${redirectUri}&state=${state}`;

res.redirect(smartThingsAuthUrl);
//res.redirect('/redirect-to-smartthings');
//console.log("client_ID"+clientID);
//console.log("client_Secret"+clientSecret);
console.log("RedirectURL"+redirectUri);
console.log("client_state: "+state);

});
the state function code is getting generated from my side
I am still getting the same error from the api.smarthtings.com “unauthorized client”
I cant find the exact issue. please help!!!

I believe that process is for OAuth integrations and API access applications. However, as you mentioned, you are using a Schema, which involves a different process. Here is a summary of the process related to Schema integration:

  1. The user will press the option on SmartThings App
  2. The previous step will trigger a request from SmartThings to your Cloud
http://your-cloud/oauth/login
  ?client_id=test1
  &redirect_uri=https://c2c-us.smartthings.com/oauth/callback
  &response_type=code
  &state=state12345
  &scope=read,%20write,%20execute
  1. During this step, you are required to manage the login and authorization processes
  2. Assuming the user grants access, you need to redirect the request back to the redirect_uri provided previously
redirect_uri=https://c2c-us.smartthings.com/oauth/callback
  ?code=123456
  &state=state12345
  1. Now SmartThings will generate a new request to get the access token
POST http://a1c7-189-203-206-33.ngrok-free.app/oauth/token
 
code=123456
&grant_type=code
&client_id=test1
&client_secret=test1

This is my github link “GitHub - eniac1546/Smartthigns-Schema-connector-switch” for this project, I am trying to call the connector.js file feature after the successful auth, and showing the device profile to the user. But I am unable to get the device profile. Its showing no device found, and its showing an empty array in the console log, can you please help in this?
–Console log
[/initiate-discovery] User is authenticated, initiating discovery.
[/initiate-discovery] Final discovery response: { devices: , addDevice: [Function: addDevice] }
HTTP GET /initiate-discovery - 304 6.286 ms

@AlejandroPadilla I am adding my app.js code for your complete reference, here I am trying to build a virtual switch with the cloud connector web hook endpoints, and google oauth2.0 for the authentication, after the user authenticate successfully the app will redirect the user to a page where the switch details and the capability i.e on, off will be available, this part of code I have added in the connector.js file. now the issue is every time I am successfully authenticating with google its redirecting me to the smart things api end point with this message “unauthorized Client”. can you tell me where am I going wrong?
App.js code

“use strict”;
const fs = require(‘fs’)

require(‘dotenv’).config();

const express = require(“express”);
const session = require(“express-session”);
const passport = require(“passport”);
const GoogleStrategy = require(“passport-google-oauth20”).Strategy;
const db = require(‘./db’);
const { StateUpdateRequest, SchemaConnector } = require(‘st-schema’);
const SmartThingsConnector = require(‘./connector’); // SmartThings integration
//const { sessionMiddleware } = require(‘./redis’); // Redis database
const morgan = require(‘morgan’); // Morgan to print logs
const path = require(“path”);
const ejs = require(“ejs”);
const crypto = require(‘crypto’);
const { fetchDeviceCapabilities } = require(‘./smartThingsUtils’); // Import the utility function
const { findVirtualSwitchDeviceId } = require(‘./smartThingsUtils’);
const app = express();
const PORT = process.env.PORT || 3000;

function generateStateParameter() {
return crypto.randomBytes(16).toString(‘hex’);
}

app.set(“view engine”, “ejs”);
app.use(express.json());
app.use(morgan(“HTTP :method :url :res[location] :status :response-time ms”));
// Serve static files from the ‘public’ folder
app.use(express.static(path.join(__dirname, “public”)));

//initialize session
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: { secure: ‘auto’ },
})
);

// Initialize passport
app.use(passport.initialize());
app.use(passport.session());

passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_REDIRECT_URI,
},
function (accessToken, refreshToken, profile, cb) {
// Use the profile information to authenticate the user
// …
cb(null, profile);
}
)
);

passport.serializeUser(function (user, cb) {
cb(null, user);
});

passport.deserializeUser(function (obj, cb) {
cb(null, obj);
});

app.get(“/login”, (req, res) => res.render(“login”));
app.get(“/dashboard”, (req, res) => req.isAuthenticated() ? res.render(“dashboard”, { user: req.user }) : res.redirect(“/login”));
app.get(“/auth/google”, passport.authenticate(“google”, { scope: [“profile”, “email”] }));

//app.get(“/auth/google/callback”,passport.authenticate(“google”, { failureRedirect: “/login” }),
//function (req, res) {
//res.redirect(“/dashboard”);
//res.redirect(“/find-virtual-switch”);
//res.redirect(“/initiate-discovery”);
// Here, instead of redirecting to /dashboard, redirect to SmartThings or another desired URL
//res.redirect(“Samsung account”);
//res.redirect(“SmartThings”);
// }
//);
app.get(“/auth/google/callback”, passport.authenticate(“google”, { failureRedirect: “/login” }), (req, res) => {
// Here, check if the user has already linked SmartThings and has a valid token
if (req.session.smartThingsAccessToken) {
res.redirect(“/initiate-discovery”);
} else {
// User is authenticated with Google but hasn’t linked SmartThings yet
res.redirect(“/auth/smartthings”);
}
});

function getUserSmartThingsAccessToken(req) {
// Check if the session exists and contains the SmartThings access token
if (req.session && req.session.smartThingsAccessToken) {
return req.session.smartThingsAccessToken;
} else {
// Log an error or handle the case where the token is not available
console.error(‘SmartThings access token not found in session.’);
return null;
}
}
app.get(‘/auth/smartthings’, (req, res) => {
if (!req.isAuthenticated()) {
return res.redirect(‘/login’);
}

const smartThingsAuthUrl = ‘Samsung account’ + new URLSearchParams({
response_type: ‘code’,
client_id: process.env.ST_CLIENT_ID,
scope: ‘r:devices:*’,
redirect_uri: process.env.ST_REDIRECT_URI,
state: ‘some_random_state’ // Ensure this is securely generated
});

res.redirect(smartThingsAuthUrl);
});

app.get(‘/smartthings/oauth/callback’, async (req, res) => {
const { code } = req.query; // The authorization code from SmartThings

try {
const response = await axios.post(‘https://api.smartthings.com/oauth/token’, {
client_id: process.env.ST_CLIENT_ID,
client_secret: process.env.ST_CLIENT_SECRET,
code: code,
grant_type: ‘authorization_code’,
redirect_uri: process.env.ST_REDIRECT_URI,
});

  const { access_token } = response.data;
  req.session.smartThingsAccessToken = access_token; // Store the token in the session

  res.redirect("/initiate-discovery");

} catch (error) {
console.error(“Failed to exchange SmartThings code for access token:”, error);
res.redirect(“/login”);
}
});

// Assuming ‘app’ is your Express app instance and SmartThingsConnector is properly imported
app.get(‘/initiate-discovery’, async (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).send(‘Unauthorized’);
}

const accessToken = getUserSmartThingsAccessToken(req);
if (!accessToken) {
return res.status(500).send(‘Failed to retrieve SmartThings access token.’);
}

console.log(“Successfully retrieved SmartThings access token:”, accessToken);

// Assuming you have an endpoint to initiate discovery in SmartThings and expecting a structure for devices
try {
// Replace yourSmartThingsDiscoveryEndpoint with the actual SmartThings endpoint for device discovery
const discoveryResponse = await axios.get(‘yourSmartThingsDiscoveryEndpoint’, {
headers: { ‘Authorization’: Bearer ${accessToken} }

});
console.log("accesstoken - ", accessToken);
// Assuming the response body contains an array of devices
const devices = discoveryResponse.data.devices.map(device => {
  return {
    externalDeviceId: device.externalDeviceId,
    deviceType: device.deviceType,
    deviceId: device.deviceId,
    manufacturerName: device.manufacturerName,
    modelName: device.modelName,
    components: device.components, // Adjust based on actual response structure
  };
});

console.log("[/initiate-discovery] Retrieved devices:", devices);

// Render the discovery view with the devices or send them as a response
res.render("discovery", { devices: devices });
// OR for API-like response:
// res.json({ devices: devices });

} catch (error) {
console.error(“Error during SmartThings device discovery:”, error);
res.status(500).send(“Error initiating device discovery process.”);
}
});

//removed the virtual switch fucntion check sublime

app.get(“/logout”, (req, res) => {req.logout(function (err) {
if (err) {
console.log(err);
} else {
res.redirect(“/login”);
}
});
});

app.post(‘/st/discovery’, (req, res) => {
SmartThingsConnector.discoveryHandler(req.headers.authorization, req.body)
.then((discoveryResponse) => {
res.json(discoveryResponse);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: ‘Discovery process failed’ });
});
});

// Endpoint for SmartThings to refresh device state
app.post(‘/st/state’, (req, res) => {
SmartThingsConnector.stateRefreshHandler(req.headers.authorization, req.body)
.then((stateRefreshResponse) => {
res.json(stateRefreshResponse);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: ‘State refresh process failed’ });
});
});

// Endpoint for SmartThings to send commands to devices
app.post(‘/st/command’, (req, res) => {
SmartThingsConnector.commandHandler(req.headers.authorization, req.body)
.then((commandResponse) => {
res.json(commandResponse);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: ‘Command process failed’ });
});
});

app.listen(PORT, () => console.log(Server listening on http://127.0.0.1:${PORT}));

I tested your connecter and I see an error in the following line:

const connector = new SchemaConnector()
    .enableEventLogging(2)
    .discoveryHandler((accessToken, response) => {
      const uniqueId = 'external-device-1';
      let d = response.addDevice(uniqueId, 'SmartPlug', '54ad9621-ce99-4c05-8a24-74c8acb0c6f3');
      d.manufacturerName('f3Sk');
      d.modelName('Switch'); 
      d.addCapability('main', 'st.switch', 1); <- this line will generate a error 
    })

The addCapability option is not valid in the Schema lib, and I’m not sure about the purpose of this line because your device profile already has this capability.

{
    "id": "54ad9621-ce99-4c05-8a24-74c8acb0c6f3",
     ...
    "components": [
        {
            "label": "main",
            "id": "main",
            "capabilities": [
                {
                    "id": "switch",
                    "version": 1,
                    "ephemeral": false
                }
            ],
            "categories": []
        }
    ],

But after correcting it, the connector works correctly, Could you explain to me the flow you are following to get this situation?

After the authentication flow, SmartThings will send a request to your Target URL and any error at that point must be reflected in the interaction result, something like this:

RESPONSE {
  "headers": {
    "schema": "st-schema",
    "version": "1.0",
    "interactionType": "commandResponse",
    "requestId": "b605b0ab-e1dd-4a93-b137-ab0c6758e122"
  },
  "deviceState": [
    {
      "externalDeviceId": "external-device-1",
      "states": [
        {
          "component": "main",
          "capability": "st.switch",
          "attribute": "switch",
          "value": "off"
        }
      ]
    }
  ]
}

I think your error is occurring on step 4, maybe this can help you, full documentation here

@AlejandroPadilla
I commented out this line “d.addCapability(‘main’, ‘st.switch’, 1);” but still its showing the same No device found error.

.

I think the problem is at this point. I’m assuming that if the user is logged in with Google, they are redirected to this endpoint. (I’m not familiar with the Google authentication process, so if I’m wrong, please let me know).

app.get(
  "/auth/google/callback",
  passport.authenticate("google", { failureRedirect: "/login" }),
  function (req, res) {
    //res.redirect('/connector');
    //res.redirect("/dashboard");
    res.redirect("/initiate-discovery");
    // Here, instead of redirecting to /dashboard, redirect to SmartThings or another desired URL
    //res.redirect("https://api.smartthings.com/oauth/authorize?client_id=YOUR-CLIENT-ID&scope=user:email");
    //res.redirect("https://api.smartthings.com/oauth/callback");
  }
);

Assuming that the user enables the access you need to redirect back to the URL provided for SmartThings:

For example, assuming that this was the first request:

https://your_server.com/login
  ?response_type=code
  &client_id=123
  &redirect_uri=https://c2c-us.smartthings.com/oauth/callback
  &scope=write,read
  &state=1234-zyxa-9134-wpst

The code of the redirect must be like this:

app.post('oauth/authotize', (req, res) => {
  const = generateCode() 
  const query_params = req.session.query_params;
  const callback_url = query_params.redirect_uri;
  let location = `${callback_url}?code=${code}`;
  location += `&state=${query_params.state}`;
  // at this point the location must look like
  // https://c2c-us.smartthings.com/oauth/callback?code=123456&state=1234-zyxa-9134-wpst
  res.redirect(302, location);
});

Note: There are other oauth providers that also you can check if you are interested, such as oauth0 or Amazon Cognito