api.colorstackatgsu.com · OAuth 2.1 / OpenID Connect
Let a ColorStack at GSU member sign into your site with one click, and receive the profile fields they agree to share — name, email, LinkedIn, GitHub, verified Discord handle — instead of asking them to type it all again.
You need a client id and client secret, which an officer issues. Email official@colorstackatgsu.com with:
There is no dynamic registration endpoint. This database holds members' resumes, and a registry anyone could add themselves to would be a resume harvester with a login button on it. Registration is a deliberate act by an officer.
Everything is discoverable. Point any conforming OpenID Connect library at the issuer and it will configure itself:
https://api.colorstackatgsu.com/.well-known/openid-configuration
| Purpose | URL |
|---|---|
| Issuer | https://api.colorstackatgsu.com |
| Authorization | /oauth2/authorize |
| Token | /oauth2/token |
| User info | /userinfo |
| Resume download | /oidc/resume |
| Public keys | /oauth2/jwks |
| Revocation | /oauth2/revoke |
Ask for the narrowest set that does your job. Consent is all or nothing: the member sees every field you asked for, with their own values, and either allows all of it or none. What you may ask for at all is fixed when an officer registers your site.
| Scope | Claims | Notes |
|---|---|---|
| openid | sub | Required. sub is a stable UUID — key your user records on it, never on the email. |
| profile | given_name, family_name, name, pronouns, major, class_year, grad_term, grad_year, school | class_year is one of Freshman, Sophomore, Junior, Senior, Graduate Student. school is always Georgia State University. |
| email, email_verified, personal_email | email is the student address and is always verified — we only admit members who hold one, so you can skip your own confirmation mail. |
|
| phone | phone_number | Digits only. Often absent: members are never required to give one. |
| socials | linkedin_url, github_url | Either may be absent. |
| discord | discord_username, discord_verified | discord_verified is proof, not a self-report: true only when the member completed verification in the chapter server. Trust it and skip asking again. |
| demographics | race_ethnicity | An array of the 2024 OMB categories. A member who declined sends ["Prefer not to say"]; keep it as given. Absent if they have not answered. |
| resume | resume_download_url, resume_filename, resume_uploaded_at | Only from /userinfo, never in a token. Download the PDF yourself — see below. |
Every claim except sub can still be missing, because a member may have left that field blank on their profile. Write your code to handle absence rather than assuming a field arrived because you asked for it.
PKCE is mandatory. Generate a code_verifier, store it in the session, and send its SHA-256 hash as the challenge.
https://api.colorstackatgsu.com/oauth2/authorize
?response_type=code
&client_id=your-client-id
&redirect_uri=https://yoursite.example/api/auth/callback
&scope=openid%20profile%20email%20socials
&state=<random, stored in the session>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256
If they have no portal session they sign in first. Then they see exactly which fields you asked for, with their own values, and choose Allow or Don't allow for the whole request. Nothing you can send skips this screen. Don't allow comes back to you as error=access_denied.
Your redirect URI receives ?code=...&state=.... Check the state matches what you stored, or you have a CSRF hole. The code is single-use and expires in minutes.
This call carries your client secret, so it must happen on your server. Never in a browser, never in a mobile app binary.
curl -X POST https://api.colorstackatgsu.com/oauth2/token \
-u "your-client-id:your-client-secret" \
-d grant_type=authorization_code \
-d code=<the code> \
-d redirect_uri=https://yoursite.example/api/auth/callback \
-d code_verifier=<the verifier you stored>
You get back an access_token, a refresh_token, and an id_token. The ID token is a signed JWT already carrying the profile claims, so for name and email you can stop here.
curl https://api.colorstackatgsu.com/userinfo \
-H "Authorization: Bearer <access_token>"
Returns the same claims, plus the ones deliberately kept out of tokens. Verify the ID token's signature against /oauth2/jwks before trusting it — any standard library does this for you.
With the resume scope granted, /userinfo returns a stable resume_download_url. Fetch the PDF from it with the same access token and store your own copy:
curl https://api.colorstackatgsu.com/oidc/resume -H "Authorization: Bearer <access_token>" -o resume.pdf
You get application/pdf with a Content-Disposition naming the member's own file. A token without the resume scope gets 403; a member with no resume on file gets 404.
resume_uploaded_at moves when the member replaces their resume. Store it alongside your copy and re-download when it changes, or you will be showing recruiters a version the member has already replaced.
Once you have the bytes, they are yours, and a member deleting their resume in the portal does not delete it from your site. Nothing in this protocol can make it.
So that becomes your obligation rather than ours: tell members plainly that you keep a copy, and give them a way to remove it. Delete copies for members who revoke your access.
| Token | Lives | Notes |
|---|---|---|
| access_token | 15 minutes | Short because it opens /userinfo, which can mint a resume link. |
| refresh_token | 30 days | Rotated on every use. Store the new one each time. |
| authorization code | minutes, single use | Replaying one invalidates the whole authorization. |
Because refresh tokens rotate, a replayed one is treated as a compromise and the authorization is dropped. If you see members getting signed out unexpectedly, check that you are persisting the newest refresh token rather than the original.
| You see | It usually means |
|---|---|
| invalid_redirect_uri | Your redirect URI does not match the registered one byte for byte. Check scheme, port and trailing slash. |
| invalid_grant | The code expired, was already used, or the code_verifier does not match the challenge you sent. |
| invalid_client | Wrong client secret, or it was not sent. Use HTTP Basic auth or form fields, not a query string. |
| invalid_scope | You asked for a scope this client is not allowed. The registration is the ceiling; email an officer to widen it. |
| A claim is missing | The member left that field blank on their profile. It is normal — handle absence. |
A member can revoke your site's access at any time from the portal. When they do, your refresh token stops working and the next refresh fails. Treat that as a sign-out, not an error to retry — and do not silently re-prompt them to reconnect.
Not every GSU student is a ColorStack member. Keep an ordinary email sign-up alongside this button, or you will lock out most of the people you are trying to reach.