Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions src/helpers/genRouteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,13 +370,24 @@ const genRouteHandler = (
/* ------------- Launch Info ------------ */
/*----------------------------------------*/

// Get launch info
const { launched, launchInfo } = getLaunchInfo(req);
// Get launch info. Public-tier routes (/api/public) are anonymous by
// definition: a live launched session must neither inject identity or
// roles into a public request nor block it via the course consistency
// check below (mirrors the /api/ttm and /api/admin path conventions)
const isPublicPath = req.path.startsWith('/api/public');
const { launched, launchInfo } = (
isPublicPath
? { launched: false, launchInfo: undefined as any }
: getLaunchInfo(req)
);
if (
// Not launched
(!launched || !launchInfo)
// Not skipping the session check
&& !skipSessionCheck
// No verified per-request course auth (a verified auth is itself proof
// of a Canvas-verified launch and may outlive the shared session)
&& !req.verifiedCourseAuth
) {
return handleError(
res,
Expand Down Expand Up @@ -432,6 +443,8 @@ const genRouteHandler = (
)
// Not skipping the session check
&& !skipSessionCheck
// No verified per-request course auth (see note above)
&& !req.verifiedCourseAuth
) {
return handleError(
res,
Expand Down Expand Up @@ -516,13 +529,70 @@ const genRouteHandler = (
}
});

/*----------------------------------------*/
/* -------- Verified Course Auth --------- */
/*----------------------------------------*/

// If the consumer app attached an already-verified per-request course
// authorization (see types/VerifiedCourseAuth), trust it over the single
// shared session for this request's course, roles, and identity
if (req.verifiedCourseAuth) {
// Make sure students don't act as other students (mirrors the
// launchInfo-based check above, which cannot run when the session is
// gone and reflects another course's roles when the session holds a
// different course)
if (
req.verifiedCourseAuth.isLearner
&& output.userId
&& output.userId !== req.verifiedCourseAuth.userId
) {
return handleError(
res,
{
message: 'We encountered a student ID mismatch. Please refresh or try the action again. Contact support if this issue persists.',
code: ExpressKitErrorCode.StudentIdMismatch,
status: 401,
},
);
}

// Course + role flags come from the verified (course-specific) auth
output.courseId = req.verifiedCourseAuth.courseId;
output.isLearner = req.verifiedCourseAuth.isLearner;
output.isTTM = req.verifiedCourseAuth.isTTM;
output.isAdmin = req.verifiedCourseAuth.isAdmin;

// Identity comes from the verified auth when the session cannot supply
// it (params still win, matching the launchInfo behavior above)
output.userId = (output.userId ?? req.verifiedCourseAuth.userId);
output.userFirstName = (
output.userFirstName
?? req.verifiedCourseAuth.userFirstName
);
output.userLastName = (
output.userLastName
?? req.verifiedCourseAuth.userLastName
);
output.userEmail = (
output.userEmail
?? req.verifiedCourseAuth.userEmail
);
output.userAvatarURL = (
output.userAvatarURL
?? 'http://www.gravatar.com/avatar/?d=identicon'
);
}

/*----------------------------------------*/
/* ----- Require Course Consistency ----- */
/*----------------------------------------*/

// Make sure the user actually launched from the appropriate course
if (
output.courseId
// No verified per-request course auth (a verified auth is itself
// proof the user launched this course)
!req.verifiedCourseAuth
&& output.courseId
&& launchInfo
&& launchInfo.courseId
&& output.courseId !== launchInfo.courseId
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import initExpressKitCollections, { getLogCollection } from './helpers/initExpre

// Import types
import CrossServerCredential from './types/CrossServerCredential';
import VerifiedCourseAuth from './types/VerifiedCourseAuth';

// Export each item
export {
Expand Down Expand Up @@ -125,6 +126,7 @@ export {
LogMetadataType,
LogFunction,
CrossServerCredential,
VerifiedCourseAuth,
// Server types
ParamType,
};
50 changes: 50 additions & 0 deletions src/types/VerifiedCourseAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Per-request verified course authorization.
*
* A consumer app sets req.verifiedCourseAuth ONLY after it has
* cryptographically verified that the requester is authorized for a specific
* course with specific roles (for example, via a signed per-tab course token
* minted at a Canvas-verified launch). When present, genRouteHandler prefers
* it over the single shared session's launchInfo for that request's course,
* roles, and identity — which is what lets multiple browser tabs work on
* different courses despite sharing one session, and lets a verified request
* outlive the session itself. When absent, behavior is unchanged.
* @author Gabe Abrams
*/
type VerifiedCourseAuth = {
// The course the requester is verified for
courseId: number,
// The user the verification was issued to
userId: number,
// True if the user is a plain course member in this course
isLearner: boolean,
// True if the user is a teaching team member in this course
isTTM: boolean,
// True if the user is a Canvas admin
isAdmin: boolean,
// Section ids of the user's launch into this course (from the launch's
// custom section_ids param), captured by the consumer at verification
// time so they stay faithful to THIS course even after the shared session
// moves on. Optional: consumers that don't use sections may omit it
sectionIds?: number[],
// The user's first name (optional: used when the session cannot supply it)
userFirstName?: string,
// The user's last name (optional: used when the session cannot supply it)
userLastName?: string,
// The user's email (optional: used when the session cannot supply it)
userEmail?: string,
};

// Augment Express so consumers can attach a verified auth to the request
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
interface Request {
// Per-request verified course authorization (set by the consumer app
// only after cryptographic verification)
verifiedCourseAuth?: VerifiedCourseAuth,
}
}
}

export default VerifiedCourseAuth;