Skip to content

feat: identities getting started #2103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/configure-generic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/configure-google.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/general-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/generic-config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/login-with-social.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/mfa-outcome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/mfa-recovery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/oidc-toggle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/one-time-codes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/otc-outcome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/passkey-prompt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/passkeys-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/privileged-sessions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/provider-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/session-cookie.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/session-lifespan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/webauthn-outcome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/identities/_assets/webauthn-passkeys.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// dashboard_handler.go
package main

import (
"encoding/json"
"net/http"
)

// dashboardHandler simply displays the session information
func (app *App) dashboardHandler(writer http.ResponseWriter, request *http.Request) {
// Get the session from context
session, err := getSession(request.Context())
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}

// Return the session data as JSON
writer.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
encoder.Encode(session)
}
8 changes: 8 additions & 0 deletions docs/identities/get-started/_common/code-examples/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module go-start

go 1.24.0

require (
github.com/ory/client-go v1.20.6 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
)
4 changes: 4 additions & 0 deletions docs/identities/get-started/_common/code-examples/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/ory/client-go v1.20.6 h1:1oUH4y1N8YCgR292FGw+atimSUgafMMIV8sZb36dH5U=
github.com/ory/client-go v1.20.6/go.mod h1:10sCeMADXhQ9GbGhgfSFfj9Pzpnue/ZAVJFSwy/L1FU=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"io"
"net/http"
)

// LoginHandler handles the /login route
func (app *App) loginHandler(writer http.ResponseWriter, request *http.Request) {
// Get cookies from the request
cookies := request.Header.Get("Cookie")

// Try to verify session with Ory
session, response, err := app.ory.FrontendAPI.ToSession(request.Context()).Cookie(cookies).Execute()

// If there's an error or session is not active, redirect to login UI
if err != nil || (err == nil && !*session.Active) {
http.Redirect(writer, request, app.tunnelUrl+"/self-service/login/browser", http.StatusSeeOther)
return
}

// If session is valid, send the session data as JSON response
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
// Use io.Copy to copy the response body to the writer
io.Copy(writer, response.Body)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this send some HTML with session info like the express one?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"log"
"net/http"
)

// LogoutHandler handles the /logout route
func (app *App) logoutHandler(writer http.ResponseWriter, request *http.Request) {
// Get cookies from the request
cookies := request.Header.Get("Cookie")

// Create a logout flow
logoutFlow, _, err := app.ory.FrontendAPI.CreateBrowserLogoutFlow(request.Context()).
Cookie(cookies).
Execute()

if err != nil {
log.Printf("Error creating logout flow: %v", err)
// Redirect to home page if there's an error
http.Redirect(writer, request, "/", http.StatusSeeOther)
return
}

// Redirect to the logout URL
http.Redirect(writer, request, logoutFlow.LogoutUrl, http.StatusSeeOther)
}
63 changes: 63 additions & 0 deletions docs/identities/get-started/_common/code-examples/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// main.go
package main

import (
"fmt"
"net/http"
"os"

ory "github.com/ory/client-go"
)

// App holds application state
type App struct {
ory *ory.APIClient
tunnelUrl string
}

func main() {
// Get tunnel port from environment or use default
tunnelPort := os.Getenv("ORY_SDK_URL")
if tunnelPort == "" {
tunnelPort = "4000"
}

// Configure baseUrl for local development
baseUrl := fmt.Sprintf("http://localhost:%s", tunnelPort)

// Configure Ory SDK
configuration := ory.NewConfiguration()
configuration.Servers = ory.ServerConfigurations{{URL: baseUrl}}

// Create Ory client
oryClient := ory.NewAPIClient(configuration)

// Initialize application
app := &App{
ory: oryClient,
tunnelUrl: baseUrl,
}

// Setup routes
mux := http.NewServeMux()
mux.HandleFunc("/login", app.loginHandler)
mux.HandleFunc("/logout", app.logoutHandler)
mux.HandleFunc("/refresh-session", app.refreshSessionHandler)
mux.Handle("/", app.sessionMiddleware(app.dashboardHandler))

// Get port from environment or use default
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}

// Print tunnel command for convenience
fmt.Printf("Starting server on port %s. Use tunnel with:\n", port)
fmt.Printf("npx @ory/cli tunnel --dev http://localhost:%s\n", port)

// Start the server
err := http.ListenAndServe(":"+port, mux)
if err != nil {
fmt.Printf("Could not start server: %s\n", err)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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
}
// highlight-start
if *session.AuthenticatorAssuranceLevel != "aal2" {
http.Redirect(writer, request, "/self-service/login/browser?aal=aal2", http.StatusSeeOther)
return
}
// highlight-end

// 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))
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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
}

// 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))
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ory_client.go
package main

import (
"os"

ory "github.com/ory/client-go"
)

// ConfigureOryClient sets up the Ory client for local development with tunnel
func ConfigureOryClient() (*ory.APIClient, string) {
baseUrl := os.Getenv("ORY_SDK_URL")

// Configure Ory SDK
configuration := ory.NewConfiguration()
configuration.Servers = ory.ServerConfigurations{{URL: baseUrl}}

// Create and return client
return ory.NewAPIClient(configuration), baseUrl
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// refresh_handler.go
package main

import (
"net/http"
)

// RefreshSessionHandler handles the /refresh-session route
func (app *App) refreshSessionHandler(writer http.ResponseWriter, request *http.Request) {
// Redirect to Ory login UI with refresh=true parameter
http.Redirect(writer, request, app.tunnelUrl+"/self-service/login/browser?refresh=true", http.StatusSeeOther)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"io"
"net/http"
)

// SignUpHandler handles the /signup route
func (app *App) signUpHandler(writer http.ResponseWriter, request *http.Request) {
// Get cookies from the request
cookies := request.Header.Get("Cookie")

// Try to verify session with Ory
session, response, err := app.ory.FrontendAPI.ToSession(request.Context()).Cookie(cookies).Execute()

// If there's an error or session is not active, redirect to login UI
if err != nil || (err == nil && !*session.Active) {
http.Redirect(writer, request, app.tunnelUrl+"/self-service/registration/browser", http.StatusSeeOther)
return
}

// If session is valid, send the session data as JSON response
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
// Use io.Copy to copy the response body to the writer
io.Copy(writer, response.Body)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
app.get("/", (req, res) => {
ory
.toSession({ cookie: req.header("cookie") })
.then((data) => res.json(data))
.catch(() =>
res.redirect(`${process.env.ORY_SDK_URL}/self-service/login/browser`),
)
})
13 changes: 13 additions & 0 deletions docs/identities/get-started/_common/code-examples/js/logout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Create logout route
app.get("/logout", async (req, res) => {
try {
// Create a logout flow
const { logout_url } = await ory.createBrowserLogoutFlow({
cookie: req.header("cookie"),
})
// Redirect to logout URL
res.redirect(logout_url)
} catch (err) {
res.redirect("/")
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const requireAuth = async (req, res, next) => {
try {
const session = await ory.toSession({ cookie: req.header("cookie") })
// highlight-start
if (session.authenticator_assurance_level === "aal2") {
req.session = session
next()
} else {
res.redirect(
`${process.env.ORY_SDK_URL}/self-service/login/browser?aal=aal2`,
)
}
// highlight-end
} 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: '[email protected]' }
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const requireAuth = async (req, res, next) => {
try {
const session = await ory.toSession({ cookie: req.header("cookie") })
req.session = session
next()
} 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: '[email protected]' }
})
Comment on lines +1 to +13
Copy link
Collaborator

@piotrmsc piotrmsc Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be re-used already in sign in /sign up example? you are also partially doing it in / handler for registration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sign and sign up sections are intentionally flat so the reader can focus on a stripped down version of the code and just care about the core. Then they progress to higher levels of abstraction which is why I created the requireAuth middleware for protecting routes

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
app.get("/refresh-session", async (req, res) => {
// Redirect to login with refresh=true parameter
res.redirect(`${baseUrl}/ui/login?refresh=true`)
})
Loading
Loading