process of verifying the identity of a user
who are you?
-
types of authentication:
- id + password
- don't allow weak passwords
- don't store passwords as plain text, hash them before storing in database
- always use bcrypt to hash passwords that will be stored in the database
- avoid MD5 and SHA-3
- id can be email, username, etc
- OAuth
- e.g. sign in with google, sign in with github, etc
- magic links
- e.g.
https://example.com/login?token=...
- e.g.
- passkeys
- id + password
-
token: string of characters that represents a user's identity, permissions or access rights
-
access token: piece of data that represents the authorization granted to a client to access specific resources and identifies the user's permissions and identity
- used to secure access to APIs and other resources
- isn't stored in the database
-
refresh token: allows a client to obtain a new access token when an existing access token expires
- doesn't provide access to resources directly
- can be revoked
- generally last longer than access token
- stored in the database (back end) and in http-only cookies (front end)
- why not increase access tokens expiration time?
- access tokens are exposed while in transit, while refresh tokens are stored securely and only used to request new access tokens
- access tokens are bearer tokens, where "possession equals authority"
- access tokens can be stolen in cross-site scripting (XSS) attacks
- bearer token: a type of access token that is sent with the request to access protected resources
- included in the Authorization header of the request, prefixed by the word "Bearer"
- Sender-Constrained Tokens (SCT): tokens that are bound cryptographic to the client through a process that uses certificates or private/public keys
- can use JWTs
-
cookie: small piece of data stored on client's browser by websites to remember stateful information
- e.g. user preferences, items in a shopping cart, refresh and access tokens, etc
- initially sent by the server
- client sends it back in afterwards
- used in browsers, not in mobile apps
Important
don't store secrets/JWTs/credentials in the browser's local storage because local storage is accessible to any javascript running on the page (including browser extensions) instead, store access tokens in variables and refresh tokens in http-only cookies (not accessible by javascript) if you store the access token in a variable, it will be lost when the component is refreshed
const [accessToken, setAccessToken] = useState(null);- benefits of storing access token with
useState:- persists after re-renders
- triggers ui updates
- works with
useEffectto fetch data
process of determining what actions a user can perform within a system
what can the user do?
- most common auth operations:
- login component
- signup component
- logout
- password recovery/reset
- get tokens
- refresh tokens
- get user data
- update user data
- block page access if unauthenticated
stateless authentication mechanism that is widely used in web development
JWTs lifecycle:
- user submits login + password
- server's handler queries the database to find the user
- compare user's hashed password in the database with password in the request
- the server generated an access token and a refresh token
- the server responds with the access token while storing the refresh token in an http-only cookie
- client stores the access token in state or context
- when access token expires, client sends refresh token to server to get a new access token (
api/refresh) - client sends the access token in the Authorization header in all future requests to the server
- on every authenticated request, server validades JWT
- eventually JWT expires and the process repeats
- JWTs are:
- base64 encoded: the payload (data) is easily readable if decoded with a Base64Url decoder
- NOT encrypted
- tamper-proof because the signature will be invalid if the header or payload is modified after token creation
- since the signature is cryptographically bound to the original content using the server's secret key
- stateless: the server doesn't need to keep track of which users are logged in via JWT
- irrevocable: can't be revoked once generated
- expirable: can be set to automatically expire after a specific time period
JSON Web Tokens consist of three parts separated by dots in the following order:
- header
- payload
- signature
example of a JWT (the token is the complete string):
-
format:
<base64url(header)>.<base64url(payload)>.<base64url(signature)> -
real example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c -
base64: enables the convertion of data (images, text, audio) into an ASCII string that can be converted back
- it's a reversible process, so you can obtain the original data from the base64 string (unlike hashing)
- use case: when you need to send that over systems that can only handle text
- a common variante is "base64 URL safe", which omits characters that might cause problems in URL path segments
Important
once the token is created by the server the data in the token cannot be tampered with because the signature is cryptographically bound to the content If anyone modifies the header or payload, the signature verification will fail since it was generated using the server's secret key This ensures data integrity and authenticity of the token
- between multiple login requests with jwt using the same user and password:
- the header doesn't change
- the payload changes even if it's the same user and password, because there's also
iat(issued at) andexp(expiration time) - the signature changes because it's generated based on the header and payload
login implementation summary:
Tip
login is basically a POST request to the server with the user's credentials that receives a JWT in return
-
what should the application's login api route do when a user logs in?
- validate credentials
- generate an access token (short-lived, e.g., 15 minutes)
- generate a refresh token (long-lived, e.g., 7 days)
- store the refresh token in the database
- send both tokens to the frontend
-
when the access token expires:
- the front end sends the refresh token to get a new access token
- if the refresh token is valid:
- issue a new access token
- optionally issue a new refresh token (rotate refresh tokens)
- if the refresh token is invalid or expired:
- force the user to log in again
implementation steps:
- install packages:
npm install jsonwebtoken bcrypt pg - create refresh token table in the database (back end)
- create
JWT_SECRETin.env- generate a random string to be the value of
JWT_SECRETwith the commandopenssl rand -base64 64 | tr -d '\n'
- generate a random string to be the value of
- test access and refresh token generation (back end)
- implement login api (back end)
- create table for refresh tokens in database (back end)
- implement endpoint to refresh refresh tokens (back end)
- implement login page (front end)
- login page generates access and refresh tokens
- store access token with
useState - store refresh token in http-only cookie
example of a refresh token table:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE refresh_tokens (
token TEXT PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP DEFAULT NULL
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE refresh_tokens;
-- +goose StatementEnd
Consists of two parts:
- The type of the token, which is JWT
- The signing algorithm being used (e.g. SHA256, RSA, etc)
Example of a header:
{
"alg": "HS256",
"typ": "JWT"
}stores claims
- claims: statements about an entity (typically, the user) and additional data
- three types of claims:
- registered claims (not mandatory but recommended)
- iss (issuer)
- exp (expiration time)
- sub (subject)
- aud (audience)
- iat (issued at)
- public claims
- can be defined at will by those using JWTs
- to avoid collisions, they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision-resistant namespace
- private claims
- custom claims created to share information between parties that agree on using them
- registered claims (not mandatory but recommended)
example of a payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}sub(subject of the token): Identifies the username: Name of the useriat(issued at): Timestamp when the token was generated
used to verify that the header and the payload weren't modified
- obtained by hashing the
header+ thepayloadwith a signing algorithm (e.g. HS256, RSA) and a secret key- secret key is used to:
- generate the signature of a jwt
- verify the signature of a jwt that was received
- secret key is used to:
- if the token is signed with a private key, it can also verify that the sender of the JWT is who it says it is
- if you send JWT through HTTP header try to prevent them from getting too big
- Some servers don't accept more than 8 KB in headers
- use safe algorithms
- HS256, RS256
- keep the secret key safe
- don't expose it in the client side
- don't store it in the database
- keep short-lived tokens
- access tokens: minutes
- refresh tokens: days, weeks, months
- use HTTPS
- allow token revocation
- validate tokens on the server, checking signature and claims
- don't add too many claims
- keep the payload small to minimize token size
- verify the
issclaim, to check if the token was issued by a trusted party - use default claims
exp(expiration time)iat(issued at)nbf(not before)sub(subject)aud(audience)
protocol that allows users to grant third-party application limited access ot their resource on another service provider's website (e.g. Google, Github)
- widely used authorization framework
- enables applications to secure designated access to user accounts on other applications and server
- decouples authentication from authorization
- OAuth 2.0 vs OpenID Connect vs SSO vs SAML:
- OAuth 2.0: authorization protocol (not authentication) that does not identity who the user is, it only authorizes access
- the app gets an access token to access the user's resources from a resource server (e.g. Google, Github, etc)
- e.g. allow a 3rd party app to read your gmail or access your github repos
- OpenID Connect (OIDC): adds authentication layer on top of OAuth 2.0
- supports authorization + authentication
- allow users to authenticate using a third-party identity provider (IdP)
- returns an ID token (JWT) that identifies the user
- e.g. "Sign in with Google" button on a website that gives an ID token
- SSO (Single Sign-On): allows users to log in once and access multiple systems without re-authenticating
- isn't a protocol
- e.g. sign in to your organization's portal and get access to email, file storage and Salesforce
- implemented using protocols like SAML, OAuth 2.0 (less common), OpenID Connect
- SAML (Security Assertion Markup Language): XML-based authentication and authorization standard
- enables SSO implementation
- older than OIDC, but still widely used
- OAuth 2.0: authorization protocol (not authentication) that does not identity who the user is, it only authorizes access
| Feature | OAuth 2.0 | OpenID Connect | SAML | SSO |
|---|---|---|---|---|
| Type | Authorization | Authentication | Authentication | Concept |
| Protocol Base | Token-based | OAuth 2.0 | XML-based | Uses SAML/OIDC |
| Token Format | Access Token | ID Token (JWT) | SAML Assertion | N/A |
| Main Use Case | API access | Login with Google | Enterprise SSO | Seamless access |
| Used In | Mobile, Web APIs | Web, Mobile, SSO | Enterprise Apps | All login systems |
| Supports Federation | No | Yes | Yes | Yes |
- user inserts credentials
- User authorizes App and delivers proof
- app sends proof of authorization to server, that sends a token in response
- whenever the user wants to access a protected resource, the user-agent should sent the JWT
-
the JWT typically is sent in the Authorization header using the Bearer schema
-
security best practices:
- don't keep tokens longer than necessary (try to minimize the time that the token is valid)
- don't store sensitive session data in browser storage (local storage, session storage, etc)
- because it's accessible by any javascript running on the page (including browser extensions)
- user clicks "Sign in with Google"
- frontend initiates the OAuth login flow
- browser redirects to Google's authorization URL
- this includes query parameters like
client_id,redirect_uri,response_type=code, andscope - traffic: AWS VPC (frontend) => Google (all egress traffic is allowed by default)
- this includes query parameters like
- user sees Google's consent screen and approves access
- if the user agrees, Google redirects them back to your app's
redirect_uriwith an authorization code in the URL
- if the user agrees, Google redirects them back to your app's
- backend server exchanges the authorization code for an access token
- backend sends a
POSTrequest tohttps://oauth2.googleapis.com/tokenwith:client_id,client_secret,code,redirect_uri, andgrant_type=authorization_code
- backend sends a
- Google responds with an access token
- The response includes:
access_token(to access Google APIs)id_token(contains user profile in JWT format)refresh_token(if applicable)expires_in
- The response includes:
- backend retrieves user info from Google
- makes
GETrequest tohttps://www.googleapis.com/oauth2/v2/userinfousing theaccess_token - Google returns user details (name, email, profile picture, etc)
- makes
- configure OAuth 2.0 client IDs here
- Resource Owner
- The person who is giving access to some portion of their account
- Example: User, John Doe
- Resource Server
- The server that holds the data that the User wants to access
- Example: Google Fit API
- Authorization Server
- The server that authenticates the User and issues the Access Token
- Example: Google's OAuth 2.0 Authorization Server
- Client
- The application that wants to access the User's data
- Example: Fitness Tracker App
- scopes are bundles of permissions asked by the client when requesting a token
- consent is the permission that the user gives to the application to access data
- resource owner: owner of the data that the client wants to access
- Example: Bob's cloud storage service is a resource server. It stores Bob's files securely. When a client requests access to specific files the resource server checks the validity of the access token before providing the requested files
- resource server: API that stores the information that the application wants to access
- client: application that wants to access your data
- public clients (client identification):
- browsers
- mobile apps
- IoT devices
- Confidential clients (client authentication):
- Also IoT devices (e.g. smart TVs)
- APIs
- public clients (client identification):
- authorization server: issues access tokens to the client after the resource owner grants authorization
- responsible for authenticating the resource owner
- usually are JSON Web Tokens (JWT)
- access token
- short lived (can last minutes or hours)
- can scale out
- can't be revoked
- refresh token
- refreshes expired access tokens
- long lived (can last days, months, or years)
- can be revoked
- to get consent and authorization from the user
- returns an authorization grant that says the user has consented to it
- then authorization is passed to the token endpoint
- processes the grant
- gives the refresh token and access token
- better auth (recommended)
- keep user data in your database
- OAuth connections
- good documentation
- quick and easy to set up
- free
- clerk
- user data is kept in clerk's database instead of yours, but you can still access it
- OAuth connections
- good documentation
- quick and easy to set up
- pay only after a certain number of users
- auth.js
- keep user data in your database
- harder to set up
- auth0
- okta
- install auth.js package
- follow auth.js auth.js documentation to setup login and signup components
- setup database
- setup .env.locale with the required environment variables listed below
Warning
if you get the error edge runtime does not support Node.js 'crypto' module
delete the middleware.ts folder and restart the server
-
real next.js full-stack project environment variables:
- database credentials: allow next.js to make requests to the database
- migrate with goose after setting
GOOSE_DBSTRINGandGOOSE_DRIVER:goose -env .env.local -dir ./src/sql/schema/. up
- migrate with goose after setting
- oauth credentials
- google OAuth credentials: allows app to use Google OAuth to authenticate
- github OAuth
- next.js secret
- next.js base URL
- database credentials: allow next.js to make requests to the database
-
.env.development: store env vars for development environment -
.env.production: store env vars for production environmentDATABASE_HOST: database credentialDATABASE_NAMEDATABASE_USERDATABASE_PASSWORDGOOSE_DBSTRING: apply migrations with gooseGOOSE_DRIVER: apply migrations with gooseAUTH_GOOGLE_ID: public id of your app (e.g. peso). access gcp console to setup OAuth with both id and secretAUTH_GOOGLE_SECRET: private key to verify identity of the appAUTH_GITHUB_ID: public id of your app (e.g. peso). access github developer settings to setup OAuth with both id and secretAUTH_GITHUB_SECRET: private key to verify identity of the appAUTH_SECRETis aliased toNEXTAUTH_SECRET: encrypts JWTsAUTH_URLis aliased toNEXTAUTH_URL: URL of your website (e.g. https://pesodevops.com), used to generate authentication routes
-
terraform.tfvars: stores terraform variables
- clerk hooks
- clerk helpers
- client-side
useUser()useClerk()useAuth()useSignIn()useSignUp()useSession()useSessionList()useOrganization()useOrganizationList()
- server-side
auth(): returnsAuthobject that contains:sessionIduserIdorgIdsessionClaimsgetToken()has(): checks if the user has authorization to access a component
currentUser: holds info about a user- route handlers
- server actions
- client-side
To obtain the values for AUTH_GOOGLE_ID (your Google Client ID) and AUTH_GOOGLE_SECRET (your Google Client Secret), you need to create an OAuth 2.0 credential in the Google Developer Console. Here’s how you can generate them:
- Go to Google Cloud Console:
- Navigate to the Google Cloud Console
- If you don’t have a project, create one by clicking on the
Select a projectdropdown at the top, thenNew Project
- Enable the Google API:
- Once your project is created, you need to enable the Google API that you want to use (for example, Google OAuth, Google Sign-In, or another API).
- Go to
APIs & Services > Libraryin the Google Cloud Console. - Search for the API you want to enable (e.g.,
Google+ APIorGoogle Identity Servicesfor login) - Click on the API and enable it by clicking the
Enablebutton
- Create OAuth 2.0 Credentials:
- In the Google Cloud Console, go to
APIs & Services > Credentials - Click on the
Create Credentialsbutton and select OAuth 2.0 Client IDs
- configure the OAuth consent screen:
- if you haven’t set up the OAuth consent screen yet, you’ll be prompted to configure it
- this screen is what users will see when they are asked to grant your app permission to access their data
- fill in the required fields (app name, email, etc.)
- leave the other options as default if you're just setting up for development
- Generate OAuth Client ID:
- under
application type, selectweb application(if you’re building a web app) - In the Authorized JavaScript origins field, add the URLs that will be using this OAuth credential (e.g.,
http://localhost:3000for local development). - In the Authorized redirect URIs field, add the URL that users will be redirected to after logging in (e.g.,
http://localhost:3000/api/auth/callback/googlefor Next.js). - Click Create.
- Get the Client ID and Client Secret:
- Once your OAuth 2.0 credentials are created, you’ll see a popup containing the Client ID and Client Secret.
- You can also find them later in
APIs & Services > Credentials
- Copy the Values:
- Copy the Client ID and Client Secret and use them in your application. These values are the ones you'll put in your environment variables (e.g.,
AUTH_GOOGLE_ID={CLIENT_ID}andAUTH_GOOGLE_SECRET={CLIENT_SECRET}).
Example:
AUTH_GOOGLE_ID=your-client-id.apps.googleusercontent.com
AUTH_GOOGLE_SECRET=your-client-secretAdditional Notes:
-
Client ID: It uniquely identifies your app to Google's OAuth servers.
-
Client Secret: A secret key used to authenticate your app when requesting tokens or making API requests on behalf of users.
-
Now, you can use these credentials in your app to integrate Google authentication
-
If you're using an authentication library like
next-author another OAuth client, you can add these values to your environment variables and pass them to the OAuth configuration.
- claims: stores statements about an entity (typically, the user) and additional data
- session ID: stateful random string that acts as pointer to the actual user record in the database
