Protected API Routes

In this chapter, you'll learn how to create protected API routes.

What is a Protected API Route?#

By default, an API route is publicly accessible, meaning that any user can access it without authentication. This is useful for public API routes that allow users to browse products, view collections, and so on.

A protected API route is an API route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access.

Protected API routes are useful for routes that require user authentication, such as creating a product or managing an order. These routes must only be accessed by authenticated admin users.

Tip: Refer to the API Reference for Admin and Store to learn how to send authenticated requests.

Default Protected Routes#

Any API route, including your custom API routes, are protected if they start with the following prefixes:

Route Prefix

Access

/admin

Only authenticated admin users can access, using a bearer token, a session, or an API key.

/store/customers/me

Only authenticated customers can access.

Opt-Out of Default Authentication Requirement#

If you create a custom API route under a prefix that is protected by default, you can opt-out of the authentication requirement by setting the Authenticate property of the route's Route to false.

Authenticate is a *bool. When it's nil, which is the default, authentication is required. The example below uses a small ptr function that returns a pointer to the value you pass:

src/api/admin/custom/route.go
func adminHelloGET(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	return res.JSON(map[string]any{"message": "Hello"})
}

// AdminHelloRoute is created under /admin, but it opts out of the
// authentication requirement that routes with this prefix have by default.
var AdminHelloRoute = medusahttp.RouteFile{
	Path: "/admin/custom",
	Route: medusahttp.Route{
		GET:          adminHelloGET,
		Authenticate: ptr(false),
	},
}

Now, any request sent to the /admin/custom API route is allowed, regardless if the admin user is authenticated.


Protect Custom API Routes#

You can protect API routes using the Authenticate middleware from the framework/src/http/middlewares package. When applied to a route, the middleware checks that:

  • The correct actor type (for example, user, customer, or a custom actor type) is authenticated.
  • The correct authentication method is used (for example, session, bearer, or api-key).

For example, you can add the Authenticate middleware to the middleware configurations of your project to protect a custom API route:

src/api/middlewares.go
// ProtectedMiddlewares protect custom API routes with the authenticate
// middleware.
var ProtectedMiddlewares = []medusahttp.MiddlewareRoute{
	{
		// Only authenticated admin users can access these routes.
		Matcher: "/custom/admin*",
		Middlewares: []medusahttp.MiddlewareFunction{
			middlewares.Authenticate(
				[]string{"user"},
				[]string{"session", "bearer", "api-key"},
				medusahttp.AuthenticateOptions{},
			),
		},
	},
	{
		// Only authenticated customers can access these routes.
		Matcher: "/custom/customer*",
		Middlewares: []medusahttp.MiddlewareFunction{
			middlewares.Authenticate(
				[]string{"customer"},
				[]string{"session", "bearer"},
				medusahttp.AuthenticateOptions{},
			),
		},
	},
}

The Authenticate function accepts three parameters:

  1. A slice of the types of actors that can authenticate. Use user for authenticating admin users, and customer for authenticating customers. You can also pass a custom actor type, or * to allow all types of users.
  2. A slice of the types of authentication methods allowed. Both user and customer scopes support session and bearer. The user scope also supports the api-key authentication method.
  3. An AuthenticateOptions struct with the following properties:
    • AllowUnauthenticated: (default: false) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too. Learn more about how the middleware handles invalid credentials in the next section.
    • AllowUnregistered (default: false): A boolean indicating whether users can access this route with a registration token, instead of an authentication token. This is useful if you have a custom actor type, such as manager, and you're creating an API route that allows these users to register themselves.
    • RequireMfa (default: true for authenticated routes): A *bool indicating whether a completed MFA challenge is required to access this route. When it's true or nil, any request from an identity with MFA enabled must have completed an MFA challenge, unless AllowUnauthenticated is set. Set it to a pointer to false to allow access regardless of MFA status, for example on the MFA challenge verification route itself.
    • RequireMfaMaxAgeSeconds: When it's set, the MFA challenge must also have been completed within the specified number of seconds.

For example, these are the options in use:

src/api/middlewares.go
// OptionsMiddlewares show the options of the authenticate middleware.
var OptionsMiddlewares = []medusahttp.MiddlewareRoute{
	{
		Matcher: "/custom/optional*",
		Middlewares: []medusahttp.MiddlewareFunction{
			middlewares.Authenticate([]string{"customer"}, []string{"session", "bearer"}, medusahttp.AuthenticateOptions{
				// Requests without credentials reach the route as guests.
				AllowUnauthenticated: true,
			}),
		},
	},
	{
		Matcher: "/custom/register*",
		Middlewares: []medusahttp.MiddlewareFunction{
			middlewares.Authenticate([]string{"manager"}, []string{"session", "bearer"}, medusahttp.AuthenticateOptions{
				// Accept a registration token, for a user that has an
				// authentication identity but no actor yet.
				AllowUnregistered: true,
			}),
		},
	},
	{
		Matcher: "/custom/sensitive*",
		Middlewares: []medusahttp.MiddlewareFunction{
			middlewares.Authenticate([]string{"user"}, []string{"session", "bearer"}, medusahttp.AuthenticateOptions{
				// Identities with MFA enabled must have completed a
				// challenge in the last ten minutes.
				RequireMfaMaxAgeSeconds: 600,
			}),
		},
	},
}

Example: Custom Actor Type#

For example, to require authentication of a custom actor type manager to an API route, or to allow multiple actor types to access an API route, pass the actor types to the Authenticate middleware:

src/api/middlewares.go
// ActorTypeMiddlewares show how to require a custom actor type, or any of
// several actor types.
var ActorTypeMiddlewares = []medusahttp.MiddlewareRoute{
	{
		Matcher: "/manager*",
		Middlewares: []medusahttp.MiddlewareFunction{
			middlewares.Authenticate([]string{"manager"}, []string{"session", "bearer"}, medusahttp.AuthenticateOptions{}),
		},
	},
	{
		Matcher: "/shared*",
		Middlewares: []medusahttp.MiddlewareFunction{
			middlewares.Authenticate([]string{"user", "customer"}, []string{"session", "bearer"}, medusahttp.AuthenticateOptions{}),
		},
	},
}

Invalid Credentials in Unauthenticated Routes#

When you set AllowUnauthenticated to true, the Authenticate middleware never rejects a request. It verifies the credentials of the request, if any, then passes the request to the next handler in both of the following cases:

  • The request carries no credentials at all.
  • The request carries credentials that fail verification, such as an expired or a tampered JWT token.

In both cases, the middleware doesn't set the AuthContext property of the request object. This is the intended behavior: the middleware's only job is to attach an authenticated actor when it can verify one, and the route decides what to do when it can't.

Warning: Because both cases look the same to your route handler, don't treat a missing AuthContext as proof that the request presented no credentials. If your route's logic differs between a signed-out request and an expired session, add the check explained next.

Reject Invalid Credentials

To answer with a 401 error when a request presents credentials that fail verification, while still allowing requests that present no credentials, add a middleware that runs after the Authenticate middleware.

For example, define the following middleware, and use it after Authenticate:

src/api/middlewares.go
// rejectInvalidAuth answers with a 401 error when a request presented
// credentials that the authenticate middleware couldn't verify.
func rejectInvalidAuth(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse, next medusahttp.NextFunction) error {
	hasToken := strings.HasPrefix(strings.ToLower(req.Header.Get("Authorization")), "bearer ")
	_, hasSession := req.Session.Get("auth_context")

	if (hasToken || hasSession) && req.AuthContext == nil {
		return common.NewMedusaError(
			common.MedusaErrorTypes.UNAUTHORIZED,
			"Your session is invalid or has expired.",
		)
	}

	next(nil)
	return nil
}

// OptionalAuthMiddleware runs rejectInvalidAuth after the authenticate
// middleware, since it reads the authentication context it sets.
var OptionalAuthMiddleware = medusahttp.MiddlewareRoute{
	Matcher: "/custom/public*",
	Middlewares: []medusahttp.MiddlewareFunction{
		middlewares.Authenticate([]string{"customer"}, []string{"session", "bearer"}, medusahttp.AuthenticateOptions{
			AllowUnauthenticated: true,
		}),
		rejectInvalidAuth,
	},
}

The middleware considers a request as presenting credentials if it has an Authorization header with a bearer token, or a session that holds an authentication context. If the request presents credentials but the Authenticate middleware didn't set AuthContext, the middleware rejects the request with a 401 error.

The order of the middlewares matters, since rejectInvalidAuth reads the AuthContext that the Authenticate middleware sets.

Now, a request with no credentials reaches the route as a guest, a request with valid credentials reaches the route with its AuthContext set, and a request with expired or invalid credentials receives a 401 error.

Override Authentication for Medusa's API Routes#

In some cases, you may want to override the authentication requirement for Medusa's API routes. For example, you may want to allow custom actor types to access existing protected API routes.

It's not possible to change the authentication middleware applied to an existing API route. Instead, you need to replicate the API route and apply the authentication middleware to it.


Access Authentication Details in API Routes#

To access the authentication details in an API route, such as the logged-in user's ID, use the AuthContext property of the MedusaRequest. It's a *dal.Record that has the following properties, among others:

  • actor_id: The ID of the authenticated user, customer, or custom actor type.
  • actor_type: The type of the authenticated actor, such as user or customer.
  • auth_identity_id: The ID of the actor's authentication identity.

AuthContext is nil if there isn't any authenticated user or customer. To read a string property of it without checking for nil, use the AuthString method of the request.

For example:

src/api/custom/admin/whoami/route.go
func whoamiGET(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	// AuthContext is nil when nobody is authenticated.
	return res.JSON(map[string]any{
		"actor_id":   req.AuthString("actor_id"),
		"actor_type": req.AuthString("actor_type"),
	})
}

In this example, you retrieve the ID and the type of the authenticated user, customer, or custom actor type from the AuthContext property.

Note: If you opt-out of authentication in a route as mentioned in the Opt-Out section, you can't access the authenticated user or customer anymore. Use the Authenticate middleware instead to protect the route.

Retrieve Logged-In Customer's Details#

You can access the logged-in customer's ID in all API routes starting with /store using the actor_id property of AuthContext. You can then use Query to retrieve the customer details, or pass the ID to a workflow that performs business logic.

For example:

src/api/custom/customer/profile/route.go
func customerProfileGET(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	filters := dal.NewRecord()
	filters.Set("id", req.AuthString("actor_id"))

	result, err := httputils.QueryOf(req.Scope).Graph(req.Ctx(), query.GraphInput{
		Entity:  "customer",
		Fields:  []string{"*"},
		Filters: filters,
	}, joiner.Options{
		// Return an error if the customer isn't found.
		ThrowIfKeyNotFound: true,
	})
	if err != nil {
		return err
	}

	return res.JSON(map[string]any{"customer": result.Data[0]})
}

In this example, you retrieve the customer's ID. Then, you resolve Query from the Medusa container with httputils.QueryOf, and use it to retrieve the customer details. The ThrowIfKeyNotFound option returns an error if the customer with the specified ID is not found.

After that, you can use the customer's details in your API route.

Retrieve Logged-In Admin User's Details#

You can access the logged-in admin user's ID in all API routes starting with /admin using the actor_id property of AuthContext, in the same way. Use user as the Entity that you retrieve with Query.

Found a problem with this page?Report Issue
Edited Nov 14·Edit this page