Accessing the `student_view_url` via Open Edx API authentication or alternatives? - authentication

I’m working on a React Native application where all the Edx course information needs to be displayed offline.
I’ve authenticated the app using oauth2 endpoint (client_id=…&grant_type=password&username=user#example.com&password=p455w0rd to {{root}}/oauth2/access_token/) and can access the API endpoints to get the users enrolled courses ({{root}}/api/enrollment/v1/enrollment) and the blocks within those courses ({{root}}/api/courses/v1/blocks/?course_id={{course_id_url_friendly}}&depth=all&nav_depth=3&return_type=list&username={{username}}). What I’m struggling to get is the contents of the HTML blocks.
I see in the official app (when viewing requests via a proxy) that it will request the actual webpage of the course, presumably the student_view_url.
Is this the only way to get that content or is there an API endpoint I can use to return the content?
If the only way is to request the rendered student_view_url, how do I access that page? The only way I can tell in the official app is it looks like it’s passing the cookies to authenticate with the studen_view_url, which it must get when it authorises the user via the oauth2 endpoint. The cookies I get don’t work though, which I’ve tested in Postman (if I use cookies that I got from browsing my Edx site in Chrome, they do work). This begs me the question, is my authentication correct as I don’t get any scope returned when the official app returns a scope filled in with read write and two others.
A sample of what my authentication returns:
{
"access_token": "a12345...",
"token_type": "Bearer",
"expires_in": 2591999,
"scope": ""
}
To reiterate;
Is there a way to request the HTML content via the API (so returned in a JSON request) rather than the student_view_url
If not then how do I authenticate to access the student_view_url?

So I worked out how to do point 2.
Before requesting any HTML content you need to hit the endpoint {{root}}/oauth2/login/ with the auth token that you provide to other endpoints. This will pass back a usable sessionid cookie that will let you access content that's not public (so long as the user has access).
As an aside, to pass the cookie through fetch() requests, I had to pass the option credentials: 'include'.

Related

Leveraging REST API's stateful token-based authentication pattern in its sole client (a web application), restricting some pages to loggedin users

I'm working on a REST API and a web application that will work off said REST API. The REST API implements a stateful token-based authentication pattern. Ultimately, allowing users to exchange their user credentials for a time-limited authentication token that identifies who they are.
So for example, a request such as:
POST: http://localhost:4000/api/v1/tokens/authentication
REQUEST BODY: {"email": "my_email_address", password": "my_password"}
would yield a response like:
"authentication_token": {
"token": "GAHZQS3IEIE6ELF7IHC6GK7M5Y",
"expiry": "2022-07-19T02:49:09.4194657+01:00"
}
If the user credentials that the user entered were incorrect, the user would not receive the authentication token.
The user needs to include the token in any further requests so that the REST API can identify the user. Some API endpoints are restricted to just logged-in users/users with certain permissions. And the token allows the REST API to know what sort of user it's dealing with. The token corresponds to a user id.
There will be a separate server from the REST API server serving the web application pages. But the pages will be populated with data from the REST API via the javascript fetch API.
I am happy with the REST API authentication. But I also need to restrict access to the web application pages based on if the user has logged in or not.
I want to leverage the existing REST API authentication but have never done something like this before so would very much appreciate any input you have.
Currently, I'm thinking of doing something like what's listed below. Do ye see any problems with it?
User accesses login page, enters credentials, and clicks 'login' button.
On clicking the 'login' button a request to the REST API is made:
POST: http://localhost:4000/api/v1/tokens/authentication
REQUEST BODY: {"email": "my_email_address", password": "my_password"}
If the credentials are correct, a token is returned, such as:
"authentication_token": {
"token": "GAHZQS3IEIE6ELF7IHC6GK7M5Y",
"expiry": "2022-07-19T02:49:09.4194657+01:00"
}
This token is added to a cookie for later use. i.e for when making another request to the REST API via the fetch API.
I'm thinking of adding another REST API endpoint that will simply validate a token i.e state whether or not the passed token is still valid and that the user is still logged in.
The webpage server can then call this endpoint to ensure the token is valid before showing a user a page that is restricted to just logged-in users.
Or maybe, instead of a validate token API endpoint, I should have a 'get user by token' endpoint. That will return user data if the token is valid. That way the webpage server could also get to know the type of user account that is associated with that user and could then also restrict pages based on the user type. It might also come in use for populating pages since nearly all pages will contain some user info e.g a profile image.

Why do we need to use the front-channel for an OAuth authorization request?

I've been struggling with this and would love to see if any OAuth experts here have an answer.
For context, I'm trying to integrate OAuth into an existing first-party (internal) front-end client that lives on a subdomain. It's a single-page application. I have an authorization server that has an /oauth2/authorize and oauth2/token endpoint and I'm working with the OAuth 2 with PKCE authorization flow.
In all the examples I've seen externally, it seems like the recommendation is to make a top-level redirect to the authorization URL initial login . And for silently re-authenticating a user (if they were already logged in), using an invisible iFrame set to the authorization URL (and postMessaging the code back to the parent window).
I'm trying to understand what prevents me from making a front-channel request to my /authorize endpoint via Javascript. Something simple like...
const { state, code } = await fetch(authorizationUrl)
For the login case, I can handle a 403 error back from the AS and then redirect them to login from the client-side. For the re-authenticating case (i.e. client has an expired refresh token but is still logged in), this is great because I just get a 200 response and the code back directly in the JSON body and I can use it immediately. There is no top-level redirect, no hassle of saving app state, etc.
It seems like as long as the AS is willing to return the { state, code } via JSON, this should work. This means that
The AS authorize endpoint must be configured to allow CORS on select origins. This seems okay in a first-party context since I know which origins I should allow.
The AS must be sent client credentials (session cookies) with the request (otherwise the AS would have no idea how to determine if the user is logged in). In JS, this would be as simple as adding credentials: true. As long as the cookie credentials have Same-Site: None and the cookie is part of the same domain (cross-domain would not work since some browsers disable cross-site cookie sharing nowadays!)
I feel like I'm missing something crucial here. But at the same time, my prototype is working, so I'd love to get some input from experienced folks here.

404s when interacting with Google Sheets REST API, 200s with Google API Explorer

I'm attempting to interact with the Google Sheets API and running into an inexplicable problem that I'm finally reaching out to see how anyone else may have tackled it. Put simply, I can use the in-page API Explorer tool with only the https://www.googleapis.com/auth/spreadsheets.readonly OAuth2 scope at https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get to query my spreadsheet (just the spreadsheet ID, leaving all other fields to empty defaults) and I'll see the 200 with the response in the bottom as expected.
Of course, I can't re-use the same access token that tool uses, but if provision an access code for the same user for my own app (same scope), and make the same GET request to https://sheets.googleapis.com/v4/spreadsheets/<spreadsheetId> in Postman (again, no other fields populated), substituting the access token into the Authentication header with Bearer <accessToken>, I get a 404.
I know the file is there - I've triple checked that I'm using the same spreadsheet ID across either request and I'm consistently getting a 404 (not a 401 or 403) indicating that my access token does authenticate.
I've tried broadening my OAuth2 scopes to include the full range listed on the API Explorer:
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/spreadsheets.readonly
Of course, I don't want to have to use all those scopes for my purposes - I'd like to use the most narrow scope possible, but I also wanted to rule out that it wasn't failing to work for some scoping scenario. No difference - still a 404 every time I make the request in Postman. I've tried issuing multiple access tokens now, using accounts.google.com to invalidate the tokens for my app between re-issuances, but to no avail.
To be clear, the Google Sheets API has been enabled for my app.
In hopes that someone else has experienced the same inability to query Google's v4 REST API despite using valid access tokens, could you share how you managed to do it?
I appreciate it!
Update:
So I've been playing around with the OAuth 2.0 Playground shared in the comments and found that the authorization endpoint I was using was identical, but the token endpoint differed. This doesn't seem to matter since I used the custom option to use the alternate endpoint and the Playground was still able to work without issue just like the API Explorer.
Using the custom entries, I also entered my own app's client ID and client secret (after registering the playground redirect URI), minimizing the differences between what I'm doing in Postman and in the various Google tools. Again, my GET request to the spreadsheet works without issue.
Just to be clear, here's what I've been doing in the Playground:
In Step 1, I've specified the https://www.googleapis.com/auth/spreadsheets.readonly scope to authorize. I click the Authorize APIs button and log in with the user account.
It returns with the authorization code, so I exchange that code for the tokens via a POST to the token endpoint.
I then make a GET request to https://sheets.googleapis.com/v4/spreadsheets/<spreadsheetId> with no additional headers and it works without issue - 200 OK and all the data I'm expecting to see.
Here my approach in Postman:
Make a GET request to:
https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&redirect_uri=https://<myDomain>/oauthResp&client_id=<appClientId>&scope=https://www.googleapis.com/auth/spreadsheets.readonly&state=abc123
Receive a response similar to the following in my browser on the redirect (since the domain intentionally 404s).
https:///oauthResp?state=abc123&code=zyx098&scope=https://www.googleapis.com/auth/spreadsheets.readonly
Make a POST request to: https://www.googleapis.com/oauth2/v4/token with a body of:
client_id=<appClientId>
client_secret=<appClientSecret>
redirect_uri=https://<myDomain>/oauthResp
grant_type=authorization_code
Receive a response similar to:
{
"access_token": "abc123",
"expires_in": 3599,
"refresh_token": "zyx098",
"scope": "https://www.googleapis.com/auth/spreadsheets.readonly",
"token_type": "Bearer"
}
Make a GET request to https://sheets.googleapis.com/v4/spreadsheets/<spreadsheetId> with a 'Content-Type' header of application/json and an 'Authorization' header of Bearer abc123 (per the access token above).
Unlike the API Explorer and the OAuth 2.0 Playground, this yields a 404 - exactly the issue I've been experiencing for no obvious reason.
Further, if I simply take the fresh access token from the Playground and drop that into Postman, I get the same 404.
Any other ideas?

Authorization between nuxtjs and the backend API

I have a Vuejs application created using Nuxtjs. I am also using Django as the backend server, and I made an API to interact with the backend server (Django) and front-end app (Vuejs/Nuxtjs). And any API related fetch are done in the AsyncData function of the page to render the data on the server-side using axios. Also, I am using json web token authentication, and the API generates a jwt token after successful login which is stored in the cookie. So on the backend, it will always check for the request's authorization header for the token. If the request is from a logged in user (authorized token) then return authenticated json data, or else return non authenticated data.
The problem:
When the user navigates to the app, I would like to check if the user is authenticated. If the user is authenticated, render the authenticated page. If not then display non authenticated page.
My thoughts:
When the fetch is done from the App on the AsyncData function, I would check whether there is any value for the cookie. If there is then send the token with the request's authorization header. But, since the page will be rendered on the server first, and not on the client side (where the cookie actually is) it will never find the token for the authorization.
How can I check if the user is already logged in or not so that I can get authenticated and non authenticated data respectively from the API?
Update
When I successfully log in (post authorized email and password), I get a json response back with the token, which I set in the cookie like this:
this.$cookie.set('my_auth_token', this.token, {expires: 15})
How can I retrieve client side cookie and into the nuxt server for server side rendering?
Cookies are exposed in the (Express) Nuxt server through middleware.
Specifically, they can be read from the req.headers.cookie property. You can see an example implementation of this in the Nuxt documentation.
Regarding your implementation: fetching the privileged data from your API using Node would seem to be the ideal way to delegate session handling to that single service (rather than both) and provide SSR for your users.
If you've chosen to instead implement your session handling on the Django service then you'll need to "forward" your cookies by passing them into your axios request headers.
I did something similar using Firebase authentication. There is an example project on Github as well as a blog entry outlining the important files and configuration used in the application.

Passport basic authentication only for API requests

I'm writing an express/node/angular application. When the client-side angular controllers need some data, they request it from the server using an endpoint that returns JSON.
This endpoint is behind some passport.authenticate('local') middleware, however this endpoint is exactly what we need for a public API.
Our API uses a passport.authenticate('basic') (basic http auth).
I'm trying to find a way to use the same URL for both, but I don't want users to my site who aren't logged in to see an http authentication window. I somehow need to tell "is this an API request, or an xhr request from the site"
For example, I want the following URL to work for a locally-authenticated user (would have been authenticated via a /login route) or for an API user:
app.get('/api/v1/tasks', passport.authenticate('basic'), tasks.list);
The only alternative I know of that works is splitting these into two different URLs, one for the site to use internally, and one for the public API. Maybe that's a better strategy for security/organization anyway?