Security News
Weekly Downloads Now Available in npm Package Search Results
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
@eropple/nestjs-auth
Advanced tools
(NestJS 7+ only) Comprehensive handling of authentication and authorization for NestJS.
@eropple/nestjs-auth
0.6.x
is being used, in anger, on multiple production apps, at my current
employer and by other NestJS users.
0.5.2
.context
at the root of the authz tree. I will
probably rethink the types expressing that API to make this easier to catch
in the future; right now it's any
and that is a smell.request.locals
. It no
longer does this.@AuthzScope()
is now stackable. If you use it multiple times on the same
handler, the scopes checked will be the union of all of them.@AuthzAdoptScopeFrom()
added. It takes a controller and the name of a
handler (which are typechecked, even though the syntax is a bit gross) and
unions the scopes specified by that handler with any scopes specified for
the current one. Thanks to Brian Kracoff @ Hydrow for the idea.@AuthnSkip
decorator. This completely omits the endpoint from any
checking, including any context functions that may attach data to your
req.locals
instance. Thanks to Brian Kracoff @ Hydrow for the contribution.unauthorizedResponse
and forbiddenResponse
to the interceptor's
options. These allow you to customize the output of 401s and 403s emitted by
@eropple/nestjs-auth
such that they can be predictable shapes in your
codebase. This feature is designed to be used with
@eropple/nestjs-openapi3 so that
you can easily provide a typed schema for your errors, but the world is your
oyster!nestjs-auth
now expects template arguments around principals and (optionally)
credentials. Take a look at the example
for details.HttpAuthnInterceptor
and HttpAuthzInterceptor
with a single
interceptor, HttpAuthxInterceptor
. This is because NestJS offers no explicit
way to guarantee that two request-scoped interceptors will run in the correct
order. Order-of-declaration works but I don't consider it sufficiently reliable
and it's easy enough to pass an always
right as part of the tree if you wish
to opt out of authz.HttpAuthnInterceptor
) will lose their
default values (where they currently map directly to IdentifiedBill
), to
encourage consumers to define their own top-level types and use them in their
applications.Authentication and authorization on the web sucks.
There, I said it.
I don't mean the initial login process, though that kinda stinks too--we've got the awesome Passport library to help us out there, though, and it really isn't that hard to write even OIDC or SAML correctly by hand. What sucks is everything past that point. There are some interesting tools out there like Open Policy Agent that are great if you want to wrangle a microserviceful universe--but most applications don't need microservices, most applications don't need to add a step to either their dev setup or their prod environment to go configure a spooky-action-at-a-distance service wedged into their environment--and most developers need something that gets out of the way so they can concentrate on building the thing they actually want to build.
In my NestJS travels, I haven't found something that hits the important bits:
@eropple/nestjs-auth
, you should have a secure-by-default auth scheme
and you should have to explicitly opt out, whether to a less secure mode for
a particular handler or to a completely unsecured mode. (This is the same
principle behind
[@eropple/nestjs-data-sec](https://github.com/eropple/nestjs-data-sec)
, for
what it's worth.)This is my take on attacking the problem. Not "once and for all," but maybe "once more for the time I'm using NestJS".
One important note: this package is only tested to work with Express. Fastify support is out of my personal scope for it; if you'd like it, I am happy to accept PRs.
It's an NPM package. It's called @eropple/nestjs-auth
. Wield your package
manager of choice and install it.
Just remember that you gotta have NestJS 6.5 or newer to make this work.
Before you read all this: code can speak for itself. Please consider
checking out
@eropple/nestjs-auth-example;
it is exhaustively commented and has end-to-end tests that demonstrate
@eropple/nestjs-auth
's completeness.
@eropple/nestjs-auth
provides the building blocks, but because of its focus on
extensibility--not prescribing to you how your domain objects should work--I'm
afraid you're going to have to do the wire-up yourself. Don't worry: it's easy,
and if it shows you some stuff you're unfamiliar with you're going to benefit
from learning how it works for your own code.
(As an aside: I've been asked why this is an interceptor rather than a guard. That's because NestJS puts guards before interceptors, and if this was written as a guard it'd mean that you couldn't put a logging interceptor around requests that are rejected. It's harder to debug and harder to reason about.)
There's perilously little magic in @eropple/nestjs-auth
. It provides one
interceptor, HttpAuthxInterceptor
, which needs to be attached to a module for
injection (we'll cover that later). These interceptors use their startup config
and a set of decorators applied to handler methods to determine who's allowed to
access what.
NOTE: Version 0.2.x used two interceptors. This proved to be not-that-great
if you wanted to use a request-scoped nestjs-auth
(for example, you use
request-scoped services in your rights tree), because even after the NestJS 6.5
fixes that allow you to properly do request-scoped interceptors the ordering of
them is undefined. In practice, they load in the order they're declared, but I
don't really want to rely on that and I don't think you should either, so 0.3.0
collapses them into a single interceptor.
Request
(from the NodeJS http
package)
into an IdentifiedExpressRequest
, which we define as adding the identity
property. This property is an IdentityBill
, which contains a principal
("who is this?"), a credential ("what says that they're them?"), and a set
of scopes that we'll use to authorize access to some resources. If the user
function determines that the identity is invalid--it's been revoked or has
expired over time, for example--then that function can return false
, and the
requestor will immediately receive 401 Unauthorized.HttpAuthnInterceptor
inspects the controller and its handler. By default,
all endpoints require authentication, but you can decorate your handlers
with @AuthnOptional()
to allow anonymous identities, with
@AuthnDisallowed()
to require them or with @AuthnSkip()
to skip the checks entirely.
If the handler's requirement matches up with the identity on the request,
the request continues; otherwise, the response is a 401 Unauthorized.@AuthnRequired | @AuthnOptional | @AuthnDisallowed | @AuthnSkip | |
---|---|---|---|---|
Good Auth | ✅ | ✅ | ❌ | ✅ |
Bad Auth | ❌ | ❌ | ❌ | ✅ |
No Auth | ❌ | ✅ | ✅ | ✅ |
@eropple/nestjs-auth
relies on three concepts for authorization: scopes,
grants, and rights.
Zero or more scopes are attached to every handler method by using the
@AuthzScope()
decorator. An identity that has both a grant and a right to
that scope is authorized to access the handler's endpoint. A list of example
scopes can be found below.
A method with zero scopes attached to it will always be allowed so long as the identity authenticates correctly.
A method with no scope decorator attached to it will, once it hits the
HttpAuthzInterceptor
, throw a 500 Internal Server Error.
Scopes provided to an identity are called grants. If a handler uses a scope
that is included in the identity's grants, then the identity is authorized to
use that handler. Since we use
[nanomatch](https://www.npmjs.com/package/nanomatch)
, you can use both *
and
**
(globstars)
in your identity's grants to expand the matches allowed.
Here are some examples of hypothetical scopes and grants, based on different resources:
user/view
- Allows viewing--for example, viewing private information such as
email address--of the singleton resource user
, implied to be "the current
user".user/edit
- Allows editing the singleton resource user
, such as editing
the user's profile.user/session/list
- Allows listing all sub-resource session
s within the
singleton resource user
. (If you made this user/session
and implied the
/list
part, you'd have surprising behavior with the next one.)user/*
(grant, not scope) - Allows any action on the singleton resource
user
. Implies both user/view
and user/edit
, but would not imply
user/session/list
(it would imply user/session
, but as we just discussed
that's not a valid scope.)user/**/*
(grant, not scope) - Allows any action on user
or subresource.file/create
- Allows the creation of a new file
resource (POST).
Presumably, the response will include the ID of that file.file/12345/view
- Allows viewing the file
resource with id 12345
.file/*/view
(grant, not scope) - Allows viewing of any file
resource, but
does not allow file/create
.**/*
(grant, not scope) - Superuser glob; allows any access to any resource.
A login scope, where you're logging in directly, will typically have this
permission unless you're implementing a GitHub-style "sudo pattern".While grants are provided by (or perhaps "on behalf of") the user, rights
determine what the user is actually allowed to access on a system level. For
example, a user might give an API token the scope file/12345/view
--but that
doesn't mean that the user is allowed to view file 12345
.
To that end, you must pass into HttpAuthzInterceptor
what we refer to as the
rights tree. This is an object tree; children map to values in the
children
If a scope is valid, its corresponding node in the rights tree will
have a right
function that returns boolean | Promise<boolean>
so that you
can check your source of truth to ensure that the identity actually does have
the right to access the OAuth2 scope that you've granted.
Once we've gotten to the authz step, you can take as guaranteed that we have
added a locals
field to the request. As such, each node may have a context
method that can test against the current request, potentially to short-circuit
and return 403 early but also to potentially store request-local data for other
uses.. For example, if a path segment is a wildcard that represents a file ID
and the file ID doesn't exist, the context
method can return a falsy value to
tell the requestor that they are unauthorized; if it does exist, the context
method can attach the file entity to request.locals
(which can then be used by
deeper parts of the rights tree or be used for parameter injection in your
handlers). context
methods never positively affirm a right, however; only a
right
method can do that.
The above example of a nonexistent file is a good time to note that neither
context
nor right
methods do not handle exceptions; throwing an
HttpException
will cause the response to be a 500 Internal Server Error. This
is a conscious decision--it might be tempting to say that we should return a 404
here, but returning a 404 here allows a potential attacker to identify when a
resource exists even if they don't have access to it. So we don't make that an
option.
You can see an example of a rights tree in Module Injection, below.
Your application's module, which we'll call MyAuthModule
for the rest of this
README, will need to tell NestJS how to build a HttpAuthxInterceptor
. We do
this with a factory
provider;
you can see how to do this in the example project's module
injection.
One helpful note: you might want to refer to NestJS's documentation on circular dependencies when writing this; forward references are a little tricky.
Once you've got your module wired up, you need to attach the authentication and the authorization interceptors to your application. There are two ways to do this; one is way better than the other.
It's a little long to put here. Please take a look at the example project.
I'm of two minds about global interceptors and guards. You have to replicate them in testing situations (please remember to add this to your E2E tests, too!) and that can lead to some confusion. On the other hand, this is the only way to assert "everything is authenticated and authorized by default".
FAQs
(NestJS 7+ only) Comprehensive handling of authentication and authorization for NestJS.
We found that @eropple/nestjs-auth demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
Security News
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.