Loading...
Loading...
Use when the user is working on who can sign in to Spree 6 and what they may do — staff roles and permission keys, secret-key scopes, registering permission scopes for an extension, 403 errors with `required_permission` / `required_scope`, storefront ownership rules (`Spree::Storefront::AccessPolicy`), custom login strategies (SSO, OIDC, Auth0/Okta/Entra/Firebase), JWT audiences, or swapping the customer/admin user class. Common phrasings include "Spree roles", "staff permissions", "add a role", "read_orders permission", "Spree.permissions.register_scope", "API key scopes", "403 Missing permission", "who can see this order", "SSO for the dashboard", "OIDC login", "custom authentication strategy", "Spree.customer_class", "admin_user_class", "CanCanCan in Spree 6". Spree 6.
npx skill4agent add spree/agent-skills spree-auth-permissions| Principal | Authenticates with | Authorized by |
|---|---|---|
Staff ( | JWT ( | Permission keys held by their roles on the current store |
| Integration (server-to-server) | Secret key | The key's scopes (same keys as roles) |
Customer ( | Publishable key | Ownership: scoped queries plus |
/api/v3/selleraud: seller_apiX-Spree-Seller-Idspree-marketplacewrite_orderscurrent_store.orderscurrent_seller.productscurrent_user.wishlistsSpree::RoleresourceSpree::StoreSpree::Sellerpermissionsread_<resource>write_<resource>write_xread_xSpree.permissions.expand_keysadminadmin403adminpermissionscan :manage, :allmutable: false/api/v3/admin/roles/api/v3/admin/permissionsread_staffwrite_staffspree api post /roles -d '{"name":"Support","permissions":["read_orders","read_customers"]}'await adminClient.roles.create({
name: 'Fulfillment staff',
permissions: ['read_orders', 'write_fulfillments', 'read_products'],
})db/seeds.rbresourcestore = Spree::Store.default
role = Spree::Role.find_or_initialize_by(name: 'support', resource: store)
role.update!(permissions: %w[read_orders read_customers write_customers])
admin = Spree.admin_user_class.find_by!(email: 'agent@example.com')
admin.add_role('support', store) # looks the role up by name + resourcewrite_seller_profileadminClient.invitations.create({ email, role_id })invitation.createdrole_idadminClient.invitations.acceptanceLink(id)GET /api/v3/admin/invitations/:id/acceptance_linkwrite_staffsellers.invitations.acceptanceLink(sellerId, id)invitations.acceptanceLink(id)spree user createadminspree-api-v3/references/scopes.mdGET /api/v3/admin/permissionsSpree::ApiKey.known_scopesread_allwrite_allpaymentsrefundsgift_cardsstore_creditsordersread_staffwrite_staff/admin_users/invitations/rolesapi_keyswebhookssettingswrite_api_keyswrite_productssettingsread_dashboard# config/initializers/spree.rb (or your engine's initializer)
Spree.permissions.register_scope(:reviews, group: :catalog, resources: -> {
[SpreeReviews::Review]
})register_scope(name, group:, resources:, write: true, audiences: [:store], read_only_for: [])group:analytics:orders:catalog:customers:sellers:loyalty:marketing:settings:accessresourceswrite: falseread_<name>audiences[:store]%i[store seller]:storeread_allwrite_allread_only_for: %i[seller]allArgumentErrorSpree.permissions.unregister_scope(:name)settingsstaffapi_keysspree.permissions_catalog.resources.<name>.label.descriptionmodule Spree
module Api
module V3
module Admin
class ReviewsController < ResourceController
scoped_resource :reviews # index/show → read_reviews; everything else → write_reviews
protected
def model_class = SpreeReviews::Review
def serializer_class = SpreeReviews::Api::V3::Admin::ReviewSerializer
def resource_permitted_attributes = %i[rating body status]
end
end
end
end
endspree:api_resourcespree-resourcescoped_resource :reviewsregister_scopeconfig/initializers/spree.rb--permission-groupcatalogconfig/locales/spree_reviews.en.ymlscoped_resourceread_allwrite_alladminread_reviewsscoped_resource :nameskip_scope_check!only:jwt_only: trueMissingScopedResourceread_indexshowread_actionswrite_cancelcurrent_api_key.has_scope?("write_orders"){ "error": { "code": "access_denied", "message": "API key lacks scope: write_orders",
"details": { "required_scope": "write_orders" } } }Spree::Ability#permission_keys"details": { "required_permission": "write_orders" }adminrequired_permission?expand=ordersread_orderspaymentspayment_splitsread_paymentscustomer(s)read_customersgift_card(s)read_gift_cardsstore_credit(s)read_store_creditsread_gift_cardsholds_permission?('read_x')GET /api/v3/admin/mepermission_keyschannel_idX-Spree-Channelspree api-key create --type secret --scopes read_orders,write_fulfillmentsSpree::Abilityability_classSpree.ability_classSpree::Dependencies.ability_classSpree::Ability# app/models/my_app/ability.rb
module MyApp
class Ability < Spree::Ability
def initialize(user, options = {}) # keep this signature
super # options[:store]; options[:resource] = the seller on the Seller API
cannot :destroy, Spree::Order # add can/cannot rules AFTER super
end
end
end
# config/initializers/spree.rb
Spree::Dependencies.ability_class = 'MyApp::Ability'required_permissionSpree::ApiKeyAbilityGET /api/v3/seller/meSpree::Abilitystorefront_access_policy.scope(Spree::Wishlist.for_store(current_store))Spree::Storefront::AccessPolicyreadable?(record, token:)writable?(record, token:)scope(relation, token:)record.customer_id == user.idX-Spree-TokenSpree::Storefront::AccessDeniedaccess_deniedclass Spree::Api::V3::Store::SavedListsController < Spree::Api::V3::Store::ResourceController
prepend_before_action :require_authentication!
protected
def scope
storefront_access_policy.scope(MyApp::SavedList.for_store(current_store))
end
def model_class = MyApp::SavedList
def serializer_class = MyApp::Api::V3::SavedListSerializer
def resource_permitted_attributes = %i[name]
endsuper# app/models/my_app/company_access_policy.rb
class MyApp::CompanyAccessPolicy < Spree::Storefront::AccessPolicy
def scope(base, token: nil)
return base.where(company_id: approver_company_ids) if base.klass <= Spree::Order && approver?
super
end
end
# config/initializers/spree.rb
Spree::Dependencies.storefront_access_policy_class = 'MyApp::CompanyAccessPolicy'validatespree-workflowsspree-b2b/customers/me/*providerPOST …/auth/loginemail| Surface | Registry | User class | JWT | Refresh token |
|---|---|---|---|---|
| Store API | | | | in the response body |
| Admin API | | | | HttpOnly cookie at |
| Seller API | | | | HttpOnly cookie at |
iss: spreeSpree::Api::Config[:jwt_secret_key]SPREE_JWT_SECRET_KEYjwt_secret_keyJWT_SECRET_KEYsecret_key_basejwt_expirationadmin_jwt_expirationSpree::Authentication::Strategies::OidcStrategy# config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.admin_authentication_strategies.add(
:entra,
Spree::Authentication::Strategies::OidcStrategy.configure(
issuer: "https://login.microsoftonline.com/#{ENV['ENTRA_TENANT_ID']}/v2.0",
client_id: ENV['ENTRA_CLIENT_ID'],
client_secret: ENV['ENTRA_CLIENT_SECRET'],
redirect_uri: 'https://shop.example.com/api/v3/admin/auth/callback/entra',
label: 'Microsoft Entra ID'
)
)
# Optional: SSO-only staff login
# Spree.admin_authentication_strategies.remove(:email)
endGET /api/v3/admin/auth/providersGET /api/v3/admin/auth/callback/:providertrust_unverified_email: false# app/models/my_app/auth/external_jwt_strategy.rb
class MyApp::Auth::ExternalJwtStrategy < Spree::Authentication::Strategies::BaseStrategy
def provider = 'external_idp'
def authenticate
payload = verify_with_jwks(params[:token]) # pin algorithms, verify iss + aud
user = find_or_create_user_from_oauth(
provider: provider,
uid: payload.fetch('sub'),
info: { email: payload['email'], first_name: payload['given_name'], last_name: payload['family_name'] }
)
success(user)
rescue JWT::DecodeError, KeyError
failure(Spree.t('api.unauthorized'))
end
private
# Yours to implement — BaseStrategy has no JWT helper. Uses the `jwt` gem.
def verify_with_jwks(token)
jwks = JWT::JWK::Set.new(MyApp::Auth.fetch_jwks) # cache the issuer's JWKS
JWT.decode(token, nil, true, algorithms: ['RS256'], jwks: jwks,
iss: ENV['IDP_ISSUER'], verify_iss: true,
aud: ENV['IDP_AUDIENCE'], verify_aud: true).first
end
end
# config/initializers/spree.rb
Rails.application.config.after_initialize do
Spree.store_authentication_strategies.add(:external_idp, MyApp::Auth::ExternalJwtStrategy)
endBaseStrategyparamsrequest_envuser_classsuccess(user)failure(msg)find_user_by_emailfind_or_create_user_from_oauth(provider:, uid:, info:, tokens: {})provider + uidSpree::UserIdentityauthenticatedef self.kind = :redirectself.labelauthorization_url(state:)callbackadd(key, klass):emailremove(key)[key]keysRails.application.config.after_initializeafter_initializeto_prepareSpree::UserIdentityprovideraccess_tokenrefresh_tokentokens:support_unencrypted_data: truespree-securitySpree::Authentication::Lockout.check(user) { user.valid_password?(pw) }:locked:valid:invalidSpree::Config[:max_failed_login_attempts][:lockout_duration]email_verified# config/initializers/spree.rb
Spree.customer_class = 'MyApp::Customer' # String, never the Class
Spree.admin_user_class = 'MyApp::StaffUser'Spree::Customerspree_customerscust_Spree::AdminUseradm_Spree::CustomerMethodsSpree::AdminUserMethodsSpree.customer_classSpree.admin_user_classSpree.user_classcustomer_classuser.*Spree::AdminUserSpree::Role.find_or_create_by!(name: 'support')store.roles.find_or_create_by!(name: 'support').update!(permissions: %w[read_orders read_customers])Spree.permissions.assign(...)Spree::PermissionSets::*assignPermissionSetsRemovedErrorspree-upgrade-5-to-6MissingScopedResourcescoped_resource :xskip_scope_check!required_permissionX-Spree-Store-Idwrite_fulfillmentswrite_shipments/admin_usersread_staffsettingsSpree::Order.find(params[:id])scopePOST /api/v3/admin/auth/password_resetsredirect_urlSpree::Stores::DashboardUrl<dashboard>/reset-passwordSameSite=None; Securespree-securitynode_modules/@spree/docs/dist/developer/core-concepts/staff-roles.mdnode_modules/@spree/docs/dist/developer/customization/permissions.mdnode_modules/@spree/docs/dist/developer/how-to/custom-api-authentication.mdnode_modules/@spree/docs/dist/developer/providers/sso.mdnode_modules/@spree/docs/dist/api-reference/admin-api/authentication.mdspree/core/lib/spree/core/permission_configuration.rbspree/core/lib/spree/core/permissions/default_catalog.rbspree/api/app/controllers/concerns/spree/api/v3/scoped_authorization.rbspree/core/app/models/spree/storefront/access_policy.rbspree-api-v3spree-securityspree-marketplacespree-b2bspree-dashboard