Forum Documentation Showcase Pricing Learn more

OAuth Like User-Agent Flow... But There's No User Endpoint... How to Configure? (Actually: Self-handled Authorization for OAuth APIs / Offline Access on Behalf of User)


#1

Howdy Bubblers,

Question time! So, I have an API that I need to authorize on behalf of users. What I need to do is basically exactly what Bubble’s “User-Agent” flow (the login type flow we’re all pretty familiar with)… except that this particular API has no user profile endpoint.

So this API cannot be used to “log the user in”, BUT, I need to authorize, get access token and come back to my own app.

Basically, if there were a user profile endpoint, I’d be done. I’ve got everything working, but of course, if I do this thru “User-Agent” flow, I get at the very last step, this error – AS WE WOULD ANTICIPATE:

To which I say, “Well, DUH, Bubble. I don’t have one and so I’ve left this blank.” Of course, this isn’t helpful to me or to Bubble. :wink:

Anyway, the question is this: For situations like this – where we are simply asking the user’s permission to connect on their behalf via an Oauth flow…

What is the correct alternative Oauth authentication scheme to use??? Neither “Password Flow” nor “Custom Token” offer exactly analogous setups to the User-Agent scheme (which, for example, has built in fields for App ID and App Secret, the name of the token, etc.).

Does anyone have an example of how one does this properly then? I’m sure it’s not “impossible”, but I’m a bit lost in terms of how one goes about creating an alternative Oauth based authentication if the built-in User-Agent flow can’t be used.

Again, it seems to me that “User-Agent” flow cannot be used here because the API I’m attempting to auth to does not have a user profile endpoint. (And, yes, I’ve confirmed this with the provider. I’ve also suggested that they really really need that… because I really really need that. But at present, it’s not an option.)

Thanks for any tips/pointers, or just the actual solution. (I can’t imagine what I’m describing is particularly rare.)

Thanks,
K


#2

I haven’t attempted it myself, but I’ve seen one guy do the “manual Oauth2 dance”, except in his case there is a login on the Bubble app and the other system.

I thought it was a rare thing, but it looks like three other people have also had a go at it, starting from this reply …


#3

Keith, is there another endpoint that would be unique to the “user”? Like account, company, project, workspace, etc?

I feel like I’ve been able to provide an endpoint that wasn’t technically for a user resource before (maybe a Google drive actually… Need to dig it up)… some resource that offered an email field and an ID field because, really, that’s what Bubble is looking for in order to associate a Bubble user with the authorized account. Note that with the built-in user-agent flow, you need to indicate the email and ID parameter names that would be found in whatever the “user” endpoint is, so as long as those parameters are present…?


#4

It is tricky, as it is just not set up to be a Social Signup system.

Bubble doesn’t need the email, but it does need a way to find the same user if they log back in. So some sort of userid.

You could probably hash it together by using a property or something, but if they removed that … it would not know they were the same user.

So the only real way to do this is to use Bubble’s normal user registration system and then “just” attach the authorisation to that.

This is doable but you need to be fairly confident in the API connector and how OAUTH works.

https://lodgable.com/api/documentation/#authentication gives all the details

You can create a URL to do the first part of the logon. This will return you a code to your redirect page as a parameter.

Then run a workflow on page load that will do the second part to get the tokens. You should be able to get the code from the URL.

Store the tokens on Bubble (not ideal, but hey).

You can then call the API with Authorization - Bearer xxxxxxxxxxxxxxx

You will also need to store the expiry date of the token. And before you do any call, check the expiry and then request another token.


#5

See this Running API workflow for user (oAuth2 User-Agent flow) while not logged in?


#6

Thanks, @mishav, @Kfawcett, @NigelG, and @romanmg for the tips. I kind of figured I would have to basically self-handle this particular auth. And I do have it to the stage where I am successfully retrieving the authorization code and exchanging it for / retrieving the first set of access and refresh tokens.

What I find, however, is that the one downfall of this in Bubble is that it becomes difficult to set up further calls in the API connector as the “Initialize Call” step gets a bit tricky. I’ve not gotten this down to a science yet…

At any rate, in addition to the very good post that @Kfawcett points to, there is this helpful post and quite clear sample project that helped me get the this point:

I think I’m still doing something derpy with the required Authorization header for this particular API, but it was getting late.

Once I have this working very slickly and smoothly, the setup and management of self-handled “user is offline” access is something I’ll create a video about. Feels to me like Bubble makes this all just a little bit harder than it potentially needs to be.


#7

Hey @keith

The initialization at the start is a lot of back and forth and depending on the api, you might have to move quickly – notably when getting the initial code which often is use once, and has a short life-span. But once you have an access-token, the initialization of all the subsequent calls is easier because they all have the same Authorization header. But then, the access token itself expires, and you have to go get a new of those. :slight_smile: So ya, it ain’t so clean. I usually build an Access token call first, then build a refresh call, and then on to all the actual calls.

For workflows, you have to build in refreshing the access tokens for some api’s (like Google.) You basically do a check of the expiration date/time and then do the refresh call if stale, just before doing your actual call.

As @NigelG points out, storing these tokens in the db isn’t ideal (+1 for his request for a secrets manager.) What happens is that in using the tokens we are exposing them to the client, while it really should remain server side. Of course, this token is FOR this user, but still I don’t like that it is exposed in the console. Maybe somebody will take the Hashi open source and bring it into Bubble server-side.

What you gain in this approach, is the ability to make calls on the user’s behalf when they are not logged-in, and you get to bifurcate login from api calls – so you can have multiple accounts across a multitude of oauth 2 providers, all tied to a single bubble user.


#8

What if you encrypt the tokens at rest in the db, then pass it to server to decrypt and make the api call. The server returns the response. This way the real token is never exposed to the client.


#9

I’d love to see an example of this.


#10

Hopefully, I can create a demo at some point with something like Dropbox to show what I mean.


#11

Why is the token exposed to the client if you only do Action workflows ?

The CODE comes back to the client but I can’t see that the token is exposed.

An alternative is to push your API call off to a webtask and have the secrets in there .

That way you don’t have token and secret in the same place.


#12

Thanks @NigelG. I never realized that the Action did not expose like a Data call does…that’s great news for the day. I guess in all my testing I was always using Data type calls which clearly show the token.


#13

I really hope that is the case !


#14

Hi, @mebeingken: thanks for the clear explication of all of that. I’m aware of it ;), but this is super-helpful for future readers of this thread.

(As for the discussion about potentially exposing access/refresh tokens, I think Nigel is correct here.)


#15

Yep, he is…as long as one stays with Action’s in workflows instead of Data calls, it looks good to me. I can work with that.


#16

Just a quick update on all of this:

I did get all of this going and learned a lot about the associated issues in doing so. I ended up with 3 distinct “APIs” in the API Connector (API call setup sections) for handling various things as each has unique authentication and header requirements in the case of this particular API:

  1. An “Auth Step 1” API setup with one API call to do the code exchange for first set of tokens. (And an associated redirect page that grabs the catch code, store the catch code momentarily in the database, refresh the page, and exchange the catch code for the first set of tokens.)

  2. A “Refresh Token” API setup with one API call to refresh tokens. (This requires no authentication at all, neither shared nor per-call.)

  3. A “Post Authorization” API setup with all of the various data calls that are the actual business end of the API. Each API call there has its own Authorization header defined per call (rather than being shared) so that they can be marked “not private” and thus dynamically accept an authorization token.

Having done this and moved on to actually using the post-authorization calls (see note ** at end), I can confirm the following:

  • Data calls in the “Post Authorization” API group, made in the page, do expose the access token in the browser. Since the Authorization header is not private (and cannot be private in this case as the “Bearer [token]” must be dynamic), this is discoverable in dev tools.

  • Action calls in the “Post Authorization” API group, made in the page, do not expose the access token in the browser. (But note next bullet about responses.)

  • It’s worth noting that a Refresh Token call made as an Action in the page does not expose the refresh token used for “self-authentication”. But note that the response from such a call is discoverable in the browser and that result of course includes the new access token and new refresh token.

So, it’s safe(ish) to do all of the above in an admin type of page (where the user whose access has been granted to one’s app is actually present and such a page is properly restricted). This is useful and expedient for certain things (like displaying info from the API in a repeating group for the user, which is easiest to get as a Data call).

But when one is performing such actions on behalf of the user, one must be mindful not to do those in the browser/on the page when the page is a public facing type page. Such calls should only be make server-side / in an API Workflow.

** Note: I had already done a lot of work with the useful API calls themselves using a different authentication scheme – this particular API has multiple modes of authorization and you can, for example, fully authenticate to the live API in a ‘single-user’ way based on username and password – but of course that’s only usable for a single-user type system and not one where you need to make calls to the API on behalf of any arbitrary user who has given you permission to do so, but it was useful for building a proof-of-concept.

As for token handling, I created a Token object that has fields for access token value, refresh token value, token expiry (seconds), expiry time (a date field that I compute when tokens are refreshed), scope (which is always returned by this particular API but at present is always null), and User (the user with whom this Token is associated).

These values, aside from User, are updated any time a Refresh Token call is issued. So token refreshing is a two-step process (1. execute the Refresh Token call on the Token, 2. make changes to the Token based on the returned values from the Refresh Token action). It’s easy to forget to do that second step, of course and one needs be mindful of that.

When a Token is first created, the User field on the Token object is set. Also, I add the Token to a field on User for convenience. Privacy rules must of course be set properly to limit access to Token objects and the token field on User.

We’ll see if I ever get around to making a helpful video about all of this. It’s a pretty complex thing and there are a lot of different variables between APIs.