Ucan extension of feathers jwt auth
npm i feathers-ucan
import {allUcanAuth, AuthService, ...
An extension of jwt authentication in feathersjs to include the added functionality of UCAN @ucans/ucans tokens. More specifically, adding capabilities.
UCAN tokens are unopinionated in general, and still emerging. There is a lot more that possibly could be done with this concept, we have built only what we have managed to use in my own current scope of project needs with this library. We have tried to leav it as unopinionated as possible!
Implementing UCAN auth in place of JWT is done, for example, like this.
``angular2html
import {AuthService, UcanStrategy} from 'feathers-ucan';
export default (app: Application) => {
const authentication = new AuthService(app);
authentication.register('jwt', new UcanStrategy());
authentication.register('local', new LocalAuth());
authentication.register('google', new GoogleStrategy(app));
authentication.register('linkedin', new LinkedinStrategy(app));
const configKey = 'authentication';
app.use('authentication', authentication);
app.configure(expressOauth());
app.service('authentication').hooks({
around: {},
after: {
create: [
...
]
}
});
}
`
See Ucan documentation for specs of ucan methods such as verify as well as types for standard ucan methods. This documentation is only to explain how these functions are used in hooks, and how we've extended them.
For ease of use within an application setting, we have provided methods for generating proper capabilities with global application settings for hierPart and scheme. These can still be overridden easily for special requirements.
capabilityParts: is just Partial where Capability is just the ucan standard Capability type. genCapability generates a full capability using the settings for default hierPart and scheme.
genCapability() returns a standard ucan Capability
You'll need the following config options under default.json authentication settings - accessible at app.get('authentication'). These could obviously be anything, but the two that are especially noteworthy are the client_ucan and ucan_aud. These are necessary for managing a ucan.
core: Our chosen implementation is to pass what we label core params - the path to "core" is configurable in the app configuration as well. This allows us to pass along key authentication data from call to call internally so we don't lose our ucan context as we go.
channels upgrade to core Since params are passed along on a per-call basis, optionally your application can store the user object at the configured [user] path (same as the path used for core params) on the connection object. This allows the same user to be used across multiple calls without needing to refresh params and reauthenticate/call the users service. We found often our users service was being called dozens of time from a single client view due to fresh params being used for many simple database calls requiring authentication.
It's worth noting that the client_ucan is typically the calling user's ucan token - so it would be accessible in vanilla feathers under context.auth.user[ucan_path]. The ucan_aud is a did and we also save this on the user - so it too would be accessible there. We simply use the core options to avoid redundant calls to authentication on internal calls.
Also worth noting is that we expose a CoreCall class that allows you to make feathers service calls and automatically pass core params along in the call. We have found this to be extensible and useful over time.
`JSON`
...
"authentication": {
"entity": "login",
"service": "logins",
"defaultScheme": "symbolDb",
"defaultHierPart": "commoncare/*",
"core_path": "core",
"ucan_path": "ucan",
"ucan_aud": "core.ucan_aud",
"client_ucan": "core.client_ucan",
...
}
where CapabilityParts is the Partial from the genCapability method, or a simplified Array<[string, string]> where the 2 elements of the array are the ucan Capability namespace and segments sequentially. There is one major modification to how UCAN works that makes it easier to utilize this in a database setting. UCAN allows for a
SUPERUSER wildcard on the can of an att/requiredCapability to pass all as long as the with elements match. See the Ability type (can) below:`
export declare type Ability = Superuser | {
namespace: string;
segments: string[];
};
`However, if you want to allow a
SUPERUSER for a given namespace, there is no allowance for this. Because of this, we have modified this implementation so that if your segments array includes a SUPERUSER, and we find a matching required capability with and can.namespace - we will transform the ucan att at that index to a SUPERUSER. In practice, here’s what methods look like (of course the mix of settings is nonsensical in normal use) that you can pass to the allUcanAuth function along with example capability configurations.
`jsx
import { anyAuth, noThrow } from '../ucan-auth'
const methods = {
create: [['logins', 'WRITE']] //standard "easy" use. All capabilities are required
patch: [['orgs', 'WRITE'], ['threads', 'READ']] //both would be required in this case unless the "or" option is passed in
remove: [{ with: { scheme: 'yourScheme', hierPart: 'application/*' }, can: { namespace: 'collection', segments: ['WRITE'] }}]
get: anyAuth,
find: noThrow
}
`*anyAuth:* provides simple naked authentication and does not enforce any ucan capabilities. In other words, it’s standard JWT auth for that method. Pass/fail for a valid token.
noThrow: is even looser - because it will not throw an error if the auth fails. It is just useful for having the
login._id present in the context.paramsThere is a flag at
context.params.canU set on all successful UCAN validations or exceptions (true if UCAN was validated or exceptions were met). On noThrow scenarios, context will be returned, but no canU flag will be set to true.The ucan valicatio result is stored for referencing details of the actual ucan validation at
context.params.ucan_auth_result.Note: the way ucans works, you cannot simply provide a “greatest ability” and have the verify method filter out lesser abilities. In other words, if you have
WRITE segment, you’d expect that to be valid for a READ requirement. However, ucans is less opinionated than that. You need to reduce the ability yourself, or it will not verify even if you have a greater ability. We have greatest ability functions, but currently the allUcanAuth method does not use it. Add only the greatest ability you wish to enforce. The UI we use for adding ucans to users does this already, so only custom scenarios should present a problem at this time. In the future, we will always reduce abilities for the greatest ability.Options
options: is an object that allows additional settings for customizing the auth experience for common exception use cases. The following are the options
`jsx
declare type UcanAuthOptions = {
creatorPass?: '*' | Array,
loginPass?: Array<[Array, Array | '*', Array|undefined]>,
or?: Array,
adminPass?: Array,
specialChange?: Array|'*',
cap_subjects?:Array
}
`$3
- creatorPass: allows for a pass if the login._id calling the method is the same as the record in question record.createdBy.login
- loginPass: allows for a free pass list of record paths that match the login._id calling the method. This is an array of loginPass config options. The first element of each array are the paths such as [owner.id] (dot notation for nested paths). In the future we expect to add $in functionality that can handle nested arrays as well (the current version will pass an array that includes the correct id, but only a flat array of simple ObjectIds - true for either the path on the login or the record, both can be an array of ids). Furthermore, if you want to match the id to something other than the _id field of the login (such as a person or other relationship) you can do so by using [owner.id/person]. The person will match owner.id on the record in question to the person path of the login making the call. Optionally as a third argument in the loginPass array you can pass an array of string ids to match against rather than checking on the context record itself.
-
- You can use a wildcard '' to check nested paths on the record subject, you can do so by using which will iterate through objects or arrays at the specified depth. So owner.contacts*id for either an array or object of many owners ("owner" is unimportant, just an example path). If the subject is an array of ids - no need to do this because owners or owners.contacts will already check an array if the field at that path is an array.'
The second element are the methods you want to allow this on ie: ['patch', 'create']Use the * superuser for allowing all methods to pass. If you want more granular field permissions - such as only allowing patch for the fields color and name, we support that. You would write the second argument of loginPass with a patch/color,name as follows (this is the full loginPass argument to avoid confusion here) [[{first argument}], ['patch/color,name', 'create']] (allowed for create as well just to illustrate how this is used);- or: explains to run the
Capability configuration passed to the allUcanAuth methods to be run as an or scenario instead of and. This is a significant extension of how ucans otherwise work. It will run multiple verify methods and if any pass, the auth will pass.
- adminPass: allow internal call overrides of ucan requirements. This is important for writing functions that internal operations may need to perform like removing a created org if a hook isn’t successful. Calling this requires passing an array of methods as the value of the admin option (Array) as well as setting context.params.admin_pass to true from within the feathers app (no client side overrides). The value of this property is an array of methods to allow admin_pass params on.
- specialChange: allow special unauthenticated changes. Pass * as the argument to allow all changes or remove. Otherwise pass a list of paths to allow changes to. Will accept common mongo operators $set $unset $addToSet $pull $push and will take either the top-level path as in topLevel.nextLevel scenarios will allow all changes if topLevel is included in the specialChange array - or the more explicit full.path.specifically
- cap_subjects: allow for a list of capability ids to be checked for a match. This is useful for checking for a specific capability that may not be in the ucan token but is rather stored in a database service (stored at 'caps' by defualt but configurable at app.get('authentication').capability_service).Example
This is a realistic example for allowing anyone to create an
org in this application, to only allow someone with universal ucan ability to WRITE to orgs or the ability to WRITE on the specific org being patched. It is using the or option to ensure either of those 2 will sufficeyou will notice the parts of the
Capability are indeed partial. Whatever parts are left out are filled in the the genCapability defaults.`jsx
import { CapabilityParts, anyAuth, hierPartBase, Capability, allUcanAuth, UcanAuthOptions } from '../ucans'const writer = [['orgs', 'WRITE']] as Array;
const deleter = [['orgs', '*']] as Array;
const ucanArgs = (context:HookContext):UcanAuthOptions => {
return {
create: anyAuth,
patch: [
['orgs', 'WRITE'] as [string, string],
{
with: {
hierPart: defaultHierPart
},
can: {
namespace:
orgs:${context.id},
segments: ['WRITE']
}
} as Partial
],
update: writer,
remove: deleter
}
}
`Then the config is used in a before all hook like this
`jsx
const authenticate = async (context:HookContext):Promise => {
return await allUcanAuth(ucanArgs(context), {or: ['patch'], adminPass: ['remove'] }})(context);
}...
before: {
all: [
authenticate,
]
}
...
`Ucan for specific database records
The challenge of giving someone rights to write to, for example, their own profile - without granting them rights to write to all profiles is easy. However, enforcing that the other way around - where a user with permission for an entire collection should also have permissions for a specific record - poses a problem.
Ucan specs don’t allow for anything but an exact match of scheme, hierPart, namespace, and segments - except for a superuser.
So we allow for this scenario by checking for each namespace to have a
namespace:id setup such as orgs:423klsjsdf3kj13lkj14`.