Hi, I just wanted to know about how we can integra...
# support-questions
a
Hi, I just wanted to know about how we can integrate supertokens with SAML. For eg. We are providing Google provider for third party auth. So if we want to provide our app as a custom application in Google workspace SSO. How will I be able to configure it with supertokens third party auth. Even if I get a minimum information from your side , it would be really helpful.
s
hey @Alen, we do support SAML
let me share the link to the documentation
let us know if you have any specific questions
r
Also, google workspaces can work with OAuth 2.0. No need to use SAML?
a
Thanks, I will look into it.
Hi, Just out of curiosity, I'm new to this space. So in my knowledge Supertokens acts as an identity provider right ? Correct me if I'm wrong. So does supertokens support SSO ?
r
We are an ID provider, but we are not an OAuth or SAML provider yet.
But we are OAuth and SAML clients
a
So if we want to introduce SAML or OAuth to our application, we have to separately configure it right with other Oauth or SAML providers ?
Supertokens will have no link in this right ?
Or do we have to add custom providers details to supertoken's recipe and configure it ?
In your doc its written "SuperTokens is not a SAML client.", that why I got confused.
s
Supertokens is not a SAML client directly, what we support is as an OAuth2 client. WIth the help of a service like BoxyHQ, which acts as an adapter, exposes OAuth2 using a SAML. our documentation explains all the necessary setup instructions for that.
a
Ok got it.
Do I have to set anything specific for google workspace to work ?
s
we have GoogleWorkspaces as an in built thirdparty provider, you could use that instead of the Google provider
a
Ok got it, any reference for that ? Or could I know the list of default in built providers list ?
s
So on the backend you would import GoogleWorkspaces instead of Google (from our examples) and use that. On the frontend, follow this guide (https://supertokens.com/docs/thirdparty/common-customizations/sign-in-and-up/custom-providers) to add a custom button component and use the id as "google-workspaces"
a
Thanks a lot.
Also just wanted to know, we have different organization clients, so how can I restrict that only that workspace domain users can login ?
s
along with clientId and clientSecret, GoogleWorkspaces accepts
hd
which can be used to restrict the domain. something like:
Copy code
ts
GoogleWorkspaces({
  clientId: '...',
  clientSecret: '...',
  hd: 'supertokens.com'
})
a
Thanks and can multiple hd parameters can be provided ?
s
no, google accepts either a '*' which allows all domains or a specific domain
a
Ok got it.
s
What you can still do is, use '*' for the hd and then override the SignInUp function and validate the email id before allowing the user to login.
a
Ok thanks, seems helpful. Will try that.
Copy code
ThirdPartyPasswordless.GoogleWorkspaces({
                    clientId: config.socialAuthConfig.google.googleClientId,
                    clientSecret: config.socialAuthConfig.google.googleClientSecret,
                    hd: 'supertokens.com'
                }),
I tried this, it listed me with only workspaces emails, but its not throwing any error with other workspace email addresses.
Like how will this hd work ? Will it only show supertokens.com domain workspace email addresses or others too and the on sign up/ in if we use other email it will throw error ?
s
Firstly, this parameter is handled by google, where it will show / allow only users from that domain. Upon callback, we also verify if the hd matches with the user's email.
a
So I kept the same hd and I used the my domain email address. ie. blocksurvey.org and it created an account for me.
s
Let me check on that and update you
a
I'm using the same google secret key and client id for workspace also. IS that the reason ?
s
I don't think that's the reason, let me get back to you
a
Sure
s
the google suggests that one should not rely on the hd parameter, as it's used only for UI optimization. Which means, you'll have to override the SignInUp function and validate the email. Seems like the google is returning the same hd that we pass in.
a
Ok got it.
When I click the google workspace, its redirecting to google auth page right, there in the payload
hd:  *
, but I have set
hd: 'supertokens.com'
s
could u share your frontend supertokens init code ?
a
Sure
Copy code
SuperTokens.init({
        appInfo: {
          apiDomain: Constants.SUPERTOKENS_SVC_URL,
          apiBasePath: "/auth",
          appName: "blocksurvey",
        },
        recipeList: [
          Session.init(),
          ThirdPartyPasswordless.init()
        ],
      });
s
are u using the web-js or the auth-react SDK?
a
web js
s
could you share your code snippet for the login action ?
a
Copy code
async handleThirdPartyAuthCallback() {
    try {
      const response = await thirdPartySignInAndUp();
      if (response.status === "OK") {
        if (response.createdNewUser) {
          // sign up successful
          this.supertokensId = response.user.id;
          this.getYourSecretKey();
          this.errorMsg = "";

          // Add user details to firebase collection
          this.emailService.addUserToSupertokensCollectionInFirebase(response.user);
        } 
      } else {
        window.alert("No email provided by social sign up. Please use another form of sign up.");
        this.emailService.redirectToPage(this.redirectTo, 'signup', this.methods);
      }
    } catch (err: any) {
      if (err.isSuperTokensGeneralError === true) {
        // this may be a custom error message sent from the API by you.
        window.alert(err.message);
      } else {
        window.alert("Oops! Something went wrong.");
      }
      this.emailService.redirectToPage(this.redirectTo, 'signin', this.methods);
    }
  }
s
not this one. the one u use to redirect
a
Copy code
async thirdPartySignUpClicked(provider) {
    try {
      const authUrl = await getThirdPartyAuthorisationURLWithQueryParamsAndSetState({
        providerId: provider,
        authorisationURL: Constants.DOMAIN_URL + "/signup",
      });

      // we redirect the user for auth.
      window.location.assign(authUrl);

    } catch (err: any) {
      if (err.isSuperTokensGeneralError === true) {
        // this may be a custom error message sent from the API by you.
        window.alert(err.message);
      } else {
        window.alert("Oops! Something went wrong.");
      }
      // redirect on failure
      this.emailService.redirectToPage(this.redirectTo, 'signup', this.methods);
    }
  }
s
sorry there was a mistake in my suggestion, plz use this in the backend:
Copy code
ts
GoogleWorkspaces({
  clientId: '...',
  clientSecret: '...',
  domain: 'supertokens.com'
})
a
Thanks I guess its working.
How can I catch this particular error ? and send custom error message ? Error: Please use a Google Workspace ID to login
r
Override the thirdPartySignInUpPOST api, call the original impl, catch the error from there, and send back a general_error status with a message to the frontend.
a
Got it. thanks.
Hi , I was going through this SAML demo with BoxyHQ. https://github.com/supertokens/jackson-supertokens-express For running this what all prerequisite is needed ?
s
Please refer our SAML guide here - https://supertokens.com/docs/thirdparty/common-customizations/saml/what-is-saml Other sections in the documentation specifies all the steps involved.
the readme of the specified repository also has the necessary steps to run the demo. let me know if you are facing any difficulty in any of the steps
a
No I wanted to know, whether I require docker in my system or postgres server running ?
Also is there any way with BoxyHq apart from self hosted. Will I get there own hosted services ?
s
for development purposes, docker is a preferred way to setup since, boxy and postgres can be run with minimal steps.
r
hey @Alen we do host boxyhq containers as well
but it's a paid option and would be happy to discuss the pricing over a call.
a
Ok got it. Let me discuss with my team and comeback to you.
r
sounds good.
a
So just wanted to know, if you host boxyhq containers for us, should we do any separate configurations with boxyhq ?
So let me clear out our requirement. BlockSurvey should be available as a cloud application to any of the SSO providers. Our enterprise customers if they want to give access to BlockSurvey for their employees, they can login using their provider and access BlockSurvey. So for eg. If our client uses Microsoft as their SSO provider, when the employees sign in using Microsoft account , it should list BlockSurvey application and on click of that if they logged in successfully, it should take them to our dashboard page. This authentication should happen using SAML.
So what should I do for this, and how I can configure it using Both Supertokens and BoxyHQ.
We would be happy if we go with BoxyHQ itself as you have support with them. I'm new to this SAML area, that why just wanted to clear things off.
r
Right. So for that, we will host boxyhq for you and you can call the APIs we expose to create a tenant and integrate with SuperTokens
i can run you through the flow over a call. I know the docs are not very explanatory for SAML at the moment.
a
yeah that would be good. So when will be a good time to have a call ?
r
can do tomorrow afternoon
2 pm IST
a
Sure, sounds good.
r
DM me your emial please. I'll send an invite
a
alen@blocksurvey.org
r
invite sent!
a
thanks a lot.
r
hey @Alen i have changed the time of the call to 3:30 pm. Hope that works.
a
Yeah it works.
r
cool
a
Hey, I wanted to know the difference between thirdPartyId and supertokensTenantId in SAMl multi tenant config
r
hey!
so third party ID will be the same for all tenants. You can keep it as "saml" for example.
and supertokensTenantId is unique ID per tenant. The value of this can be the same as what you gave to boxyhq
a
is it same id which you gave saml-jackson for frontend backend ?
Or can it be separate
r
> is it same id which you gave saml-jackson for frontend backend ? Yea same. So if the
id
in frontend and backend is called
saml-jackson
, this should also be called that.
(im talking about thirdPartyId)
a
Yeah understood. thanks.
From frontend how will I pass the client id and secret key using userContext ?
Which APi I have to override for that in frontend
r
u don't need to pass it from the frontend.
you only need to pass the tenantid from the frontend
a
Ok so no need of usercontext then right. Directly if I send the tenant id I can fetch the client details using this get API
"/recipe/thirdparty/tenant/config"
. Am I correct?
r
yes correct
and you can override the getAuthorisationURLGET API and signInUpPOST API to get the tenantId, query the core to get the clientId and secret, and then add those to the user context
Then you can read the userContext in the custom provider
a
how can I add it to userContext?
r
just something like input.userContext.client_id = ...
a
Ok sorry, Got little confused, In frontend which function I have to send tenant id ? Is it
getThirdPartyAuthorisationURLWithQueryParamsAndSetState()
. do I have to override this function and then send tenant id using usercontext ?
Do I have to send something like this ?
Copy code
const authUrl = await getThirdPartyAuthorisationURLWithQueryParamsAndSetState({
        providerId: provider,
        providerClientId : 'tenantId',

        authorisationURL: Constants.DOMAIN_URL + "/signup",
      });
r
> Ok sorry, Got little confused, In frontend which function I have to send tenant id ? Is it getThirdPartyAuthorisationURLWithQueryParamsAndSetState(). do I have to override this function and then send tenant id using usercontext ? Yes in this and in the signinup function. In both the cases, you use the preAPIHook to add the tenantId to the request body.
the providerClientId shouldn't be the tenantId.
on the frontend, see how the preAPiHook is defined here to add the tenantId to the requests being made: https://github.com/supertokens/jackson-supertokens-express/blob/main/app/src/App.js#L52
and we override the API on the backend to read the request in these two APIs: https://github.com/supertokens/jackson-supertokens-express/blob/main/api/app.js#L43
a
Copy code
const authUrl = await getThirdPartyAuthorisationURLWithQueryParamsAndSetState({
        providerId: provider,
        options: {
          preAPIHook: async (context: any) => {
            let url = new URL(context.url);
            let action = context.action;

            if (action === 'GET_AUTHORISATION_URL') {
              let tenantId = 'test'
              localStorage.setItem("saml-tenant-id", tenantId)
              url.searchParams.append('tenant', tenantId);
              url.searchParams.append('product', 'saml-jackson');
            }

            if (action === 'THIRD_PARTY_SIGN_IN_UP') {
              let tenantId = localStorage.getItem("saml-tenant-id");
              url.searchParams.append('tenant', tenantId);
              url.searchParams.append('product', 'saml-jackson');
            }

            return {
              requestInit: context.requestInit,
              url: url.href,
            };
          },
        },
        authorisationURL: Constants.DOMAIN_URL + "/signup",
      });
Is this right way for sending tenant id ? My recipe is thirdpartypasswordless. Is this the action 'THIRD_PARTY_SIGN_IN_UP' used for my recipe.
r
So the pre APi hook here will only have the request for
GET_AUTHORISATION_URL
since you are just calling that function
and wherever you call signinup function, there too add a pre APi hook
a
Ok,
let action = context.action;
this is undefined.
action is not present in the context
r
yea. So you don't need action here cause there is just one API call being made
the example app link i sent above is adding the pre api hook on the supertokens init level
here you are adding it on the function level
therefore no context required
a
Got it, thanks for your support.
`authorisationUrlGET: async (input) => { input.userContext.request = input.options.req.original; let request = input.userContext.request; let tenant = request === undefined ? "" : request.query.tenant; let product = request === undefined ? "" : request.query.product; const profile = await axios({ method: 'get', url:
https://2b2279d150ffbe9aaad08e5-us-east-1.aws.supertokens.io:3567/recipe/thirdparty/tenant/config?thirdPartyId=${product}&supertokensTenantId=${tenant}
, headers: { 'api-key': 'secret', }, }); let client_id = profile.data.config.client_id; let client_secret = profile.data.config.client_secret; input.userContext.client_id = client_id; input.userContext.client_secret = client_secret; return originalImplementation.authorisationUrlGET(input); },`
I'm trying to fetch the tenant's client id and secret and I'm able to fetch it but somehow its throwing some other error like
Copy code
TypeError: Cannot read properties of undefined (reading 'params')
    at Object.<anonymous> (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:45:75)
    at Generator.next (<anonymous>)
    at C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:30:75
    at new Promise (<anonymous>)
    at __awaiter (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:12:16)
    at Object.authorisationUrlGET (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:42:20)
    at Object.proxy._call (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-js-override\lib\build\index.js:56:56)
    at Object.ret.<computed> [as authorisationUrlGET] (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-js-override\lib\build\getProxyObject.js:27:29)
    at Object.authorisationUrlGET (C:\BlockSurvey Projects\blocksurvey-supertoken-function\index.js:408:59)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
index.js:408:59
this line points to
return originalImplementation.authorisationUrlGET(input);
Am I doing something wrong ?
r
this does seem fine on first look
the error might be coming from the custom provider implementation - what is happening in there?
a
in frontend ?
`{ id: "saml-jackson", get: async (redirectURI, authCodeFromRequest, userContext) => { console.log(userContext); let client_id = userContext.client_id; let client_secret = userContext.client_secret; return { accessTokenAPI: { url:
https://7193f3e14ea770f517937-us-east-1.aws.supertokens.io:5125/api/oauth/token
, params: { client_id, client_secret, grant_type: "authorization_code", redirect_uri: redirectURI || "", code: authCodeFromRequest || "", } }, ...`
This is the custom provider code
r
does the execution come in the
get
function? Oh and also, the get function is not async. So you should remove the
async
keyword
a
Thanks the issue is solved, async was the issue.
r
ah nice
a
But dont know after successful auth when signinup method is called it throws this error
Copy code
TypeError: Cannot read properties of undefined (reading 'startsWith')
    at isUsingDevelopmentClientId (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:199:22)
    at Object.<anonymous> (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:90:25)
    at Generator.next (<anonymous>)
    at C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:30:75
    at new Promise (<anonymous>)
    at __awaiter (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:12:16)
    at Object.signInUpPOST (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-node\lib\build\recipe\thirdparty\api\implementation.js:85:20)
    at Object.proxy._call (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-js-override\lib\build\index.js:56:56)
    at Object.ret.<computed> [as thirdPartySignInUpPOST] (C:\BlockSurvey Projects\blocksurvey-supertoken-function\node_modules\supertokens-js-override\lib\build\getProxyObject.js:27:29)
    at Object.thirdPartySignInUpPOST (C:\BlockSurvey Projects\blocksurvey-supertoken-function\index.js:441:73)
My thirdPartySignInUpPOST override func looks like this.
Copy code
thirdPartySignInUpPOST: async function (input) {
                            try {
                                if (originalImplementation.thirdPartySignInUpPOST) {
                                    input.userContext.request = input.options.req.original
                                    return await originalImplementation.thirdPartySignInUpPOST(input);
                                }
                            } catch (err) {
                                if (err.message === "Cannot sign up as email already exists") {
                                    // this error was thrown from our function override above.
                                    // so we send a useful message to the user
                                    return {
                                        status: "GENERAL_ERROR",
                                        message: "Seems like you already have an account with another method. Please use that instead."
                                    }
                                }
and this is my frontend call for the same
Copy code
const response = await thirdPartySignInAndUp({
        options: {
          preAPIHook: async (context: any) => {
            let url = new URL(context.url);
            let tenantId = 'alen'
            localStorage.setItem("saml-tenant-id", tenantId)
            url.searchParams.append('tenant', tenantId);
            url.searchParams.append('product', 'saml-jackson');

            return {
              requestInit: context.requestInit,
              url: url.href,
            };
          },
        }
r
where do you call startsWith function?
a
no where,
index.js:441:73
points at
return await originalImplementation.thirdPartySignInUpPOST(input);
r
hmm. Can you wait for @sattvikc to help out over here?
he should be free sometime tomorrow
a
Sure. got just had one doubt. why do we have to pass the tenant details in signinuppost call ?
Only for authorisationUrlGET its required right ?
to fetch the client id and secret
r
cause you need client id and secret in that API also
a
Ok, so in which call should I pass those values, I think I wasn't passing client details for that call.
That maybe the issue
r
what do you mean which call?
on the frontend or backend?
a
backend
r
no i mean you are supposed to override
thirdPartySignInUpPOST
which you have done
and add to the userContext client_id and client_secret
before calling originalimpl
a
but in this I'm not passing client id and secret again right
Copy code
thirdPartySignInUpPOST: async function (input) {
                            try {
                                if (originalImplementation.thirdPartySignInUpPOST) {
                                    input.userContext.request = input.options.req.original
                                    return await originalImplementation.thirdPartySignInUpPOST(input);
                                }
                            } catch (err) {
                                if (err.message === "Cannot sign up as email already exists") {
                                    // this error was thrown from our function override above.
                                    // so we send a useful message to the user
                                    return {
                                        status: "GENERAL_ERROR",
                                        message: "Seems like you already have an account with another method. Please use that instead."
                                    }
                                }
r
oh you need to
so copy the code from the other APi where you are calling the core to get the client_id and secret
and put it in this APi as well
a
will this affect other third party auth ?
r
it will. But you can modify the user context only if the request has the tenantId input. If it doesn't then things will continue to work the way it used to
a
Ok got it.
One more doubt, it was from Wilson's side. Is there any way to figure out if any employee from the tenants side has been removed, as there will be supertokens session available right, so we would like to restrict those users.
r
removed as in the user has been deleted or just removed from that tenant's org?
a
Like yeah if the users are removed from the org
r
hmm. So at that time of removing them from the org, you could loop through all the user's session and call the revokeSession function on them
and then this would log them out when they do a next refresh
a
Ok got it. Thanks.
r
one more thing, in the custom provider, can you show me the impelmentation for the getProfileInfo function?
a
`getProfileInfo: async (accessTokenAPIResponse) => { const profile = await axios({ method: 'get', url:
https://7193f3e10df3d70f517937-us-east-1.aws.supertokens.io:5225/api/oauth/userinfo
, headers: { Authorization:
Bearer ${accessTokenAPIResponse.access_token}
, }, }); return { id: profile.data.id, email: { id: profile.data.email, isVerified: true } }; }`
r
right. So change the last part of
id: profile.data.id,
to be something like
id: profile.data.id + "|" + tenantId,
and you can get the tenantId from the userContext in the same way of how you get the client_id and secret
the reason to do this is so that if a person signs into a different tenant using their same SAML login, it will treat them as different users.
from the userContext
a
Sorry , I misunderstood, thanks.
The email is coming like this, we would like to fetch the email address of user.
r
. Can you logout the profile object in getProfileInfo function?
a
yeah I'm able to logout and login to same account
so it gets updated on supertokens user dashboard
r
Right but related to the email issue, can you console log the profile object? In the getProfileInfo function
a
sure
Copy code
data: {
    raw: {
      id: '1dda9fb491dc01bd24d2423ba2f22ae561f56ddf2376b29a11c80281d21201f9',
      email: 'jackson@example.com',
      firstName: 'jackson',
      lastName: 'jackson',
      'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier': 'jackson@example.com'
    },
    id: '1dda9fb491dc01bd24d2423ba2f22ae561f56ddf2376b29a11c80281d21201f9',
    email: 'jackson@example.com',
    firstName: 'jackson',
    lastName: 'jackson',
    idHash: 'e47ade86ba357f1041be45f8477f5b790309b3e7',
    requested: {
      client_id: '5283edb8634c7ca6c0431a7049cacf0fedba31ef',
      state: '74d99b0c7d3f94f1ec290',
      redirect_uri: 'http://localhost:4200/signup',
      tenant: 'alen1',
      product: 'Blocksurvey',
      scope: []
    }
  }
r
So you do set the email to
profile.data.email
which is
jackson@example.com
in this case
so it should make the user have that email ID
a
but you asked me to override the email value right. `getProfileInfo: async (accessTokenAPIResponse) => { const profile = await axios({ method: 'get', url:
https://7193f3e14ef517937-us-east-1.aws.supertokens.io:5225/api/oauth/userinfo
, headers: { Authorization:
Bearer ${accessTokenAPIResponse.access_token}
, }, }); console.log(profile); return { id: profile.data.id, email: { id: profile.data.id + "|" + tenantId, isVerified: true } };`
r
oh no. Overrde the
id
value:
Copy code
return {
  id: profile.data.id + "|" + tenantId,
  email: {
    id: profile.data.email,
    isVerified: true
  }
};
a
Ok got it.
Also one quick help. Can you help me how to separate the flow for thirdparty and this saml flow. `authorisationUrlGET: async (input) => { input.userContext.request = input.options.req.original; let request = input.userContext.request; if (request.query.tenant && request.query.product) { let tenant = request === undefined ? "" : request.query.tenant; let product = request === undefined ? "" : request.query.product; const profile = await axios({ method: 'get', url:
${config.supertokensConfig.connection_url}/recipe/thirdparty/tenant/config?thirdPartyId=${product}&supertokensTenantId=${tenant}
, headers: { 'api-key': config.supertokensConfig.api_key, }, }); let client_id = profile.data.config.client_id; let client_secret = profile.data.config.client_secret; input.userContext.client_id = client_id; input.userContext.client_secret = client_secret; return await originalImplementation.authorisationUrlGET(input); } },`
r
so if
if (request.query.tenant && request.query.product) {
is not true, you should just call the original implementation
a
I had tried this, but if you see this signinup call for google auth, query params are being added which is for saml.
r
yea do thats a frontend issue
so add those query params only if the thirdpartyid is related to saml
a
Thanks, its fixed now.
Hopefully, we will be going live with SAML by next week. Thanks for your support.
r
Nice!
2 Views