Multi-factor authentication
This guide shows you how to enable multi-factor authentication (MFA) for your Ory project.
- Ory Console
- Ory CLI
To enable MFA using the Console:
- Log in to your Ory Console
- Select your workspace and project
- Navigate to the Authentication tab
- Click on Two-factor auth in the sidebar
Configure One-Time Codes
In the Two-factor auth settings, you can enable and configure One-Time Codes for multi-factor authentication:
Toggle the "Enable one-time code multi factor authentication" toggle to allow users to receive one-time codes for MFA.
To enable MFA using the CLI:
- First, get your current identity configuration:
# List all available workspaces
ory list workspaces
# List all available projects
ory list projects --workspace <workspace-id>
# Get the configuration
ory get identity-config --project <project-id> --workspace <workspace-id> --format yaml > identity-config.yaml
- Edit the configuration file to enable One-Time Codes and set MFA requirements:
# Enable One-Time Codes for MFA
selfservice:
methods:
code:
enabled: true # Enable the one-time code method
mfa_enabled: true
- Update your configuration:
ory update identity-config --project <project-id> --workspace <workspace-id> --file identity-config.yaml
This configuration forces users to provide the highest authentication factor available to access their account settings. For example, users without a second factor configured can access settings after they sign in with their password, but users that have a second factor set up (such as a TOTP app) will need to complete the second factor challenge.
What users will see
When MFA is enabled, users will see a second authentication screen after logging in:
Check AAL
Authentication Authorization Level (AAL) is a concept that describes the strength of the authentication factor used to access a resource.
- aal1: Password/OIDC
- aal2: Password/OIDC and one-time code
To check the AAL of the current session, use the authenticator_assurance_level
on the toSession
method.
- Expressjs
- Next.js
- Go
const requireAuth = async (req, res, next) => {
try {
const session = await ory.toSession({ cookie: req.header("cookie") })
if (session.authenticator_assurance_level === "aal2") {
req.session = session
next()
} else {
res.redirect(
`${process.env.ORY_SDK_URL}/self-service/login/browser?aal=aal2`,
)
}
} catch (error) {
res.redirect(`${process.env.ORY_SDK_URL}/self-service/login/browser`)
}
}
app.get("/", requireAuth, (req, res) => {
res.json(req.session.identity.traits) // { email: 'newtestuser@gmail.com' }
})
export async function middleware(request: NextRequest) {
console.log("Middleware executed for path:", request.nextUrl.pathname)
try {
const session = await ory.toSession({
cookie: request.headers.get("cookie") || "",
})
if (session.authenticator_assurance_level === "aal2") {
return NextResponse.next()
} else {
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_ORY_SDK_URL}/self-service/login/browser?aal=aal2`,
)
}
} catch (error) {
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_ORY_SDK_URL}/self-service/login/browser`,
)
}
}
// Configure which routes to protect
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|public).*)"],
}
// middleware.go
package main
import (
"context"
"errors"
"log"
"net/http"
ory "github.com/ory/client-go"
)
func (app *App) sessionMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
log.Printf("Checking authentication status\n")
// Pass cookies to Ory's ToSession endpoint
cookies := request.Header.Get("Cookie")
// Verify session with Ory
session, _, err := app.ory.FrontendAPI.ToSession(request.Context()).Cookie(cookies).Execute()
// Redirect to login if session doesn't exist or is inactive
if err != nil || (err == nil && !*session.Active) {
log.Printf("No active session, redirecting to login\n")
// Redirect to the login page
http.Redirect(writer, request, "/self-service/login/browser", http.StatusSeeOther)
return
}
if *session.AuthenticatorAssuranceLevel != "aal2" {
http.Redirect(writer, request, "/self-service/login/browser?aal=aal2", http.StatusSeeOther)
return
}
// Add session to context for the handler
ctx := withSession(request.Context(), session)
next.ServeHTTP(writer, request.WithContext(ctx))
}
}
func withSession(ctx context.Context, v *ory.Session) context.Context {
return context.WithValue(ctx, "req.session", v)
}
func getSession(ctx context.Context) (*ory.Session, error) {
session, ok := ctx.Value("req.session").(*ory.Session)
if !ok || session == nil {
return nil, errors.New("session not found in context")
}
return session, nil
}
// Dashboard page protected by middleware
mux.Handle("/", app.sessionMiddleware(app.dashboardHandler))
User flow
- The user enters their username/password or uses another primary authentication method
- They see the MFA challenge screen
- A one-time code is sent to their email
- After entering the valid code, they gain access to the application or protected settings