Issuing JWT access token using node-oidc-provider in authorization_code workflow - node-oidc-provider

The following is my configuration for the oidc provider.
I add resource indicators under the features. But it does not working at all. How can it issues JWT access token using node-oidc-provider in authorization_code workflow?
The provider version is 7.11.4.
clients: [
{
client_id: '0oa5pa23mzKPWYtvV5d7',
redirect_uris: ['https://jwt.io', 'http://localhost:3000/login/callback'], // using jwt.io as redirect_uri to show the ID Token contents
response_types: ['code'],
application_type: 'web',
token_endpoint_auth_method: 'none',
scope: 'openid offline_access profile email',
grant_types: ['authorization_code', 'refresh_token'],
},
],
routes: {
authorization: '/oauth2/v1/authorize',
token: '/oauth2/v1/token',
revocation: '/oauth2/v1/revoke',
userinfo: '/oauth2/v1/userinfo',
},
clientBasedCORS(ctx, origin, client) {
if (client.clientId === '0oa5pa23mzKPWYtvV5d7' && origin === 'http://localhost:3000') {
return true;
}
return false;
},
pkce: {
required: () => true,
},
interactions: {
url(ctx, interaction) { // eslint-disable-line no-unused-vars
return `/interaction/${interaction.uid}`;
},
},
cookies: {
keys: ['some secret key', 'and also the old rotated away some time ago', 'and one more'],
},
claims: {
address: ['address'],
email: ['email', 'email_verified'],
phone: ['phone_number', 'phone_number_verified'],
profile: ['birthdate', 'family_name', 'gender', 'given_name', 'locale', 'middle_name', 'name',
'nickname', 'picture', 'preferred_username', 'profile', 'updated_at', 'website', 'zoneinfo'],
},
features: {
resourceIndicators: {
getResourceServerInfo: () => ({
audience: 'solid',
accessTokenTTL: 2 * 60 * 60, // 2 hours
accessTokenFormat: 'jwt',
jwt: {
sign: { alg: 'ES256' },
},
}),
},
},

defaultResource also needs to be updated. In case of jwt, this function has to return proper value. By default, it returns undefined.
resourceIndicators: {
defaultResource: (ctx, client, oneOf) => {
if (oneOf) return oneOf;
return client['access_token_type'] === 'opaque' ? undefined : `https://${clientId}.com`;
},
getResourceServerInfo: (ctx, resourceIndicator, client) => {
return ({
scope: client.scope,
accessTokenTTL: 2 * 60 * 60,
accessTokenFormat: 'jwt',
});
}
},

add also useGrantedResource to the resourceIndicators
useGrantedResource: (ctx, model) => {
// #param ctx - koa request context
// #param model - depending on the request's grant_type this can be either an AuthorizationCode, BackchannelAuthenticationRequest, RefreshToken, or DeviceCode model instance.
return true;
}

To issue accessToken for usage on protected resources you can:
Ensure you have your api: api1.example.com, api2.example.com, api3.example.com
Ensure you have defined extraClientMetadata
/** #See https://github.com/panva/node-oidc-provider/tree/main/docs#extraClientMetadata */
/** Allows for custom client metadata to be defined, validated, manipulated as well as for existing property validations to be extended. Existing properties are snakeCased on a Client instance (e.g. client.redirectUris), new properties (defined by this configuration) will be avaialable with their names verbatim (e.g. client['urn:example:client:my-property']) */
module.exports = {
/**
* isInternalClient: true | false, wether the client is for first party or for third party
*
* resourcesScopes: ressource scope clients is allowed to requested for
*
* allowedResources: ressource server client is allowed to request token for
*/
properties: ['allowedResources', 'resourcesScopes', "isInternalClient"],
validator: function extraClientMetadataValidator(ctx, key, value, metadata) {},
};
You can register your apis on the idP
// src/oidc/config/resources-servers.js
// Define list of resources server
let resourcesList = {
};
// Register api 1 on the IdProvider
// Assume process.env.API1_BASE_URL = https://api1.example.com
resourcesList[process.env.API1_BASE_URL] = {
// These scope is accept the by the resource server to make authorization decisions
scope: 'offline_access api:query api:get api:post api:patch api:delete',
// Audience
audience: process.env.API1_BASE_URL,
// accessTokenFormat?: 'opaque' | 'jwt' | 'paseto'
// Please see https://github.com/panva/node-oidc-provider/tree/main/docs#getresourceserverinfo sections Resource Server (API) for more customization
accessTokenFormat: 'opaque',
};
// Register api 2 on the IdProvider
// Assume process.env.API2_BASE_URL = https://api2.example.com
resourcesList[process.env.API2_BASE_URL] = {
scope: 'offline_access api:query api:get api:post api:patch api:delete',
audience: process.env.API2_BASE_URL,
accessTokenFormat: 'opaque',
};
// Register api 3 on the IdProvider
// Assume process.env.API3_BASE_URL = https://api3.example.com
resourcesList[process.env.API3_BASE_URL] = {
scope: 'offline_access api:query api:get api:post api:patch api:delete',
audience: process.env.API3_BASE_URL,
accessTokenFormat: 'opaque',
};
Now configure each client with it allowed resources server it can request accessToken for and related resource scope:
// src/oidc/config/clients.js
{
client_id: "client1",
// ...
// Other client meta data
// ...
// These below are clientExtraMetadata. Mandatory for api access_token usage
allowedResources: [process.env.API1_BASE_URL, process.env.API2_BASE_URL],
resourcesScopes: "offline_access api:query api:get api:post api:patch api:delete",
isInternalClient: true
},
{
client_id: "client2",
// ...
// Other clie
// ...
// These below are clientExtraMetadata. Mandatory for api access_token usage
allowedResources: [process.env.API1_BASE_URL, process.env.API3_BASE_URL],
resourcesScopes: "offline_access api:query api:get api:post api:patch api:delete",
isInternalClient: true
}
Define your config.features.resourceIndicator properly like according to previous stuffs
/** #See https://github.com/panva/node-oidc-provider/tree/main/docs#featuresresourceindicators */
resourceIndicators: {
enabled: true,
defaultResource(ctx, client, oneOf) {
return Array.isArray(ctx.oidc.params?.resource)
? ctx.oidc.params?.resource[0]
: ctx.oidc.params?.resource;
},
useGrantedResource: async function useGrantedResource(ctx, model) {
return true;
},
getResourceServerInfo(ctx, resourceIndicator, client) {
// Ensure resourceIndicator is provided and this resource exist
if (!resourceIndicator || !resourcesList[resourceIndicator]) {
throw new errors.InvalidRequest("invalid_request", "Invalid resource server");
}
// Get this resource infos
var targetResourceServer = resourcesList[resourceIndicator];
// Client request access_token for api must defined these 2 metadata: allowResources, ressourcesScopes
if (!Array.isArray(client.allowedResources) || !(client.allowedResources.includes(resourceIndicator))) {
throw new errors.InvalidClientMetadata("invalid_client_metadata", "allowedResources & allowedResources are mandatory or you cannot request access token for this server");
}
// Now ensure client get access_token for scope it not defined
let clientAllowedScope = "";
if (client.resourcesScopes) {
var scopesList = client.resourcesScopes.split(' ');
clientAllowedScope = scopesList.filter((scopeItem) => {
return targetResourceServer.scope.includes(scopeItem);
});
} else {
throw new errors.InvalidClientMetadata("invalid_client_metadata", "Please specify at least one scope");
}
console.log(">>----Client ressource allowed:", client.allowedResources)
console.log(">>----Client ressource scopes:", client.resourcesScopes)
console.log(">>----Target ressource server is:", targetResourceServer)
console.log(">>----resourceIndicator is :", resourceIndicator)
console.log(">>----Client Scope allowed:", clientAllowedScope.join(' '))
// Update the acces_token ressource to issued
targetResourceServer.scope = clientAllowedScope.join(' ');
return targetResourceServer;
},
}
Now you can issue accessToken targeting your apis even as jwt, passeto or opaque

Related

Strapi returns 404 for custom route only when deployed to Heroku

I have created a custom route in Strapi v4 called "user-screens". Locally I hit it with my FE code and it returns some data as expected. However when I deploy it to Heroku and attempt to access the endpoint with code also deployed to Heroku it returns a 404. I've tailed the Heroku logs and can see that the endpoint is hit on the server side, but the logs don't give anymore info other than it returned a 404.
I am doing other non custom route api calls and these all work fine on Heroku. I am able to auth, save the token, and hit the api with the JWT token and all other endpoints return data. This is only happening on my custom route when deployed to Heroku. I've set up cors with the appropriate origins, and I am wondering if I need to add something to my policies and middlewares in the custom route. I have verified the permissions and verified the route is accessible to authenticated users in the Strapi admin.
Here is my route:
module.exports = {
routes: [
{
method: "GET",
path: "/user-screens",
handler: "user-screens.getUserScreens",
config: {
policies: [],
middlewares: [],
},
},
],
};
And my controller:
"use strict";
/**
* A set of functions called "actions" for `user-screens`
*/
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
ctx.send(result);
});
},
};
For anyone facing this, the answer was to change how I returned the ctx response from a 'send' to a 'return' from the controller method. I am not sure why this works locally and not on Heroku, but this fixes it:
New controller code:
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
return strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
return result;
})
.catch((error) => {
return error;
});
},
};

Inject token from auth0 to React-admin Hasura data provider

This example demonstrates how to use Auth0 with react-admin. It is working as expected.
We are trying to adjust it so it will use the hasura data provider. We've created a new file dataProvider.js that will construct the data-provider:
import buildHasuraProvider from "ra-data-hasura";
import { ApolloClient, InMemoryCache } from "#apollo/client";
export const initDataProvider = async (token) => {
const client = new ApolloClient({
uri: process.env.REACT_APP_GRAPHQL_URI,
headers: {
Authorization: `Bearer ${token}`,
},
cache: new InMemoryCache(),
});
const dataProvider = await buildHasuraProvider({ client });
return dataProvider;
};
However, we are missing the JWT token which is created as part of the Auth0 authentication process. We do not know how to get the token in order to initialize the data provider with it. Does react-admin know how to do it on its own? if not, how do we access the JWT token to do it ourselves manually?
This is the authProvider source-code:
import authConfig from "./authConfig";
import {Auth0Client} from '#auth0/auth0-spa-js';
const auth0 = new Auth0Client({
domain: authConfig.domain,
client_id: authConfig.clientID,
redirect_uri: authConfig.redirectURI,
cacheLocation: 'localstorage',
useRefreshTokens: true
});
export default {
// called when the user attempts to log in
login: (url) => {
if (typeof url === 'undefined') {
return auth0.loginWithRedirect()
}
return auth0.handleRedirectCallback(url.location);
},
// called when the user clicks on the logout button
logout: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) { // need to check for this as react-admin calls logout in case checkAuth failed
return auth0.logout({
redirect_uri: window.location.origin,
federated: true // have to be enabled to invalidate refresh token
});
}
return Promise.resolve()
})
},
// called when the API returns an error
checkError: ({status}) => {
if (status === 401 || status === 403) {
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) {
return Promise.resolve();
}
return auth0.getTokenSilently()
})
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => {
return Promise.resolve()
},
};
It is unclear to us if there is a point where we can extract the token from.
getTokenSilently should give you back the token.
You'll have to structure your React app such that you have access to the result of this method before you construct your data provider.

AngularFireAuthGuard redirectUrl after login

I use firebase and AngularFireAuthGuard to protect specific routes, so that only authenticated users are allowed to access them.
In particular, my MainComponent and MgmtComponent should only be accessible to AUTHENTICATED users.
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['/login']);
const routes: Routes = [
{ path: 'teams/:teamId/sessions/:sessionId',
component: MainComponent,
canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }
},
{ path: 'mgmt',
component: MgmtComponent,
canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }
},
{
path: 'login',
component: LoginComponent
}
];
My Problem is, that the user is not redirected back to the originally requested URL, after a successful login.
So what I want/expect is:
user goes to /mgmt
as the user is not authenticated he is automatically redirected to /login
user authenticates (e.g. via google or Facebook OAuth)
user is automatically redirected back to the originally requested page (/mgmt)
Steps 1-3 work fine, but step 4 is missing.
Now that the feature request is in, you can do this using the auth guard. However, the docs are unclear, so here is how I did it.
/** add redirect URL to login */
const redirectUnauthorizedToLogin = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
return redirectUnauthorizedTo(`/login?redirectTo=${state.url}`);
};
/** Uses the redirectTo query parameter if available to redirect logged in users, or defaults to '/' */
const redirectLoggedInToPreviousPage = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
let redirectUrl = '/';
try {
const redirectToUrl = new URL(state.url, location.origin);
const params = new URLSearchParams(redirectToUrl.search);
redirectUrl = params.get('redirectTo') || '/';
} catch (err) {
// invalid URL
}
return redirectLoggedInTo(redirectUrl);
};
This is an open feature request, the angularfire team is working on it: https://github.com/angular/angularfire/pull/2448
Meanwhile I found this workaround:
In the app-routing-module.ts instead of
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['/login']);
I use following to store the url in the sessionStorage:
const redirectUnauthorizedToLogin = (route: ActivatedRouteSnapshot) => {
const path = route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/');
return pipe(
loggedIn,
tap((isLoggedIn) => {
if (!isLoggedIn) {
console.log('Saving afterLogin path', path);
sessionStorage.setItem('afterLogin', path);
}
}),
map(loggedIn => loggedIn || ['/login'])
);
};
In the LoginComponent I read the value from the session storage to redirect:
sessionStorage.getItem('afterLogin');
this.router.navigateByUrl(redirectUrl);

How to change password after logging in?

I used the following code to change the password, but I get "Request failed with status code 400". Can someone give me an indication of where the problem is?
axios.post ('http: // localhost: 1337 / auth / reset-password', {
       code: '',
       password: '1234567',
       passwordConfirmation: '1234567',
     }
     , {
       headers: {
           Authorization: `Bearer $ {this.currentUser.jwt}`
       }
     }
     ) .then (response => {
       // Handle success.
       console.log ('Your user \' s password has been changed. ');
     })
     .catch (error => {
       // Handle error.
       console.log ('An error occurred:', error);
     });
   }
Thanks in advance
Another alternative way is by using a password reset controller. The scenario is by POST a password object to http://localhost:1337/password, the controller will validate the current password then update the password with given newPassword, and return a new jwt token.
We will post a password object as follows:
{
"identifier": "yohanes",
"password": "123456789",
"newPassword": "123456",
"confirmPassword": "123456"
}
The steps are:
Create password reset route /api/password/config/routes.json:
{
"routes": [
{
"method": "POST",
"path": "/password",
"handler": "password.index"
}
]
}
Create password reset controller at /api/password/controllers/password.js
module.exports = {
index: async ctx => {
return 'Hello World!';
}
}
Note: Don't forget to enable password index at Roles -> Permission -> Application.
Point Postman to http://localhost:1337/password. The response will display the text Hello World!.
Update the password controller:
module.exports = {
index: async ctx => {
// Get posted params
// const params = JSON.parse(ctx.request.body); //if post raw object using Postman
const params = ctx.request.body;
// The identifier is required.
if (!params.identifier) {
return ctx.badRequest(
null,
formatError({
id: 'Auth.form.error.email.provide',
message: 'Please provide your username or your e-mail.',
})
);
}
// Other params validation
.
.
.
// Get User based on identifier
const user = await strapi.query('user', 'users-permissions').findOne({username: params.identifier});
// Validate given password against user query result password
const validPassword = await strapi.plugins['users-permissions'].services.user.validatePassword(params.password, user.password);
if (!validPassword) {
return ctx.badRequest(
null,
formatError({
id: 'Auth.form.error.invalid',
message: 'Identifier or password invalid.',
})
);
} else {
// Generate new hash password
const password = await strapi.plugins['users-permissions'].services.user.hashPassword({password: params.newPassword});
// Update user password
await strapi
.query('user', 'users-permissions')
.update({ id: user.id }, { resetPasswordToken: null, password });
// Return new jwt token
ctx.send({
jwt: strapi.plugins['users-permissions'].services.jwt.issue({ id: user.id }),
user: sanitizeEntity(user.toJSON ? user.toJSON() : user, { model: strapi.query('user', 'users-permissions').model }),
});
}
}
}
Once the password object posted, the controller will update the user password and return a newly created jwt token.
The complete code can be found here. Tested on Strapi v.3.3.2
You will have to use the PUT /users/:id route (from the User API)
If you want this route used by a user, you will have to create a isOwner policy and apply it to this route.
To let only the current user udpate it's own password and not all users password.
Here some documentation:
Create a policy
Get the current user in the request
Customize the User plugin
Here is yohanes's solution adapted to Strapi v4
For some reason the Strapi team has removed the hashPassword method of the users-permission.user service, so we need to generate the hash ourselves now. For this we use the same having method as v3 did. We need to import bcrypt like this: const bcrypt = require("bcryptjs");
Out new changePassword needs to look something like this:
async changePassword(ctx) {
const userId = ctx.request.body.userId;
const currentPassword = ctx.request.body.currentPassword;
const newPassword = ctx.request.body.newPassword;
if (!userId || !currentPassword || !newPassword) {
return ctx.throw(400, "provide-userId-currentPassword-newPassword");
}
let user = await strapi
.query("plugin::users-permissions.user")
.findOne({ id: userId });
const validPassword = await strapi
.service("plugin::users-permissions.user")
.validatePassword(currentPassword, user.password);
if (!validPassword) {
return ctx.throw(401, "wrong-current-password");
} else {
// Generate new hashed password
const password = bcrypt.hashSync(newPassword, 10);
user = await strapi.query("plugin::users-permissions.user").update({
where: { id: user.id },
data: { resetPasswordToken: null, password },
});
// Return new jwt token
ctx.send({
jwt: strapi.service("plugin::users-permissions.jwt").issue({
id: user.id,
}),
user: sanitizeOutput(user),
});
}
},

Cannot connect Ember Simple Auth and DRF Token Auth

I have a trouble with Ember Simple Auth.
I'm trying to connect my server-side application, which working on Django 1.9 with DRF, and client-side which working on Ember 2.2.
On server side I'm obtaining token on 'http://localhost:8000/api-token-auth/'. Function requires two args from request: "username" and "password". But Ember Simple Auth send POST request with args: "username[identification]" and "password[password]", and server returns "400". I think that problem with arguments keys.
POST request
Responce
I tried to change .authenticate method in oauth2-password-grant.js(i can't write custom authenticator because i'm newbee in javascript), but nothing changed.
Manually POST request returns expected answer.
Please tell me the way to solve this problem.
And please forgive me for my english.
authenticate(identification, password, scope = []) {
return new RSVP.Promise((resolve, reject) => {
const data = { 'grant_type': 'password', username: identification, password };
const serverTokenEndpoint = this.get('serverTokenEndpoint');
const scopesString = Ember.makeArray(scope).join(' ');
if (!Ember.isEmpty(scopesString)) {
data.scope = scopesString;
}
this.makeRequest(serverTokenEndpoint, data).then((response) => {
run(() => {
const expiresAt = this._absolutizeExpirationTime(response['expires_in']);
this._scheduleAccessTokenRefresh(response['expires_in'], expiresAt, response['refresh_token']);
if (!isEmpty(expiresAt)) {
response = Ember.merge(response, { 'expires_at': expiresAt });
}
resolve(response);
});
}, (xhr) => {
run(null, reject, xhr.responseJSON || xhr.responseText);
});
});
},
My variant:
const data = { 'grant_type': 'password', 'username': identification, 'password': password };
authenticate: function () {
// var username = this.getProperties('username');
// var password = this.getProperties('password');
const {username, password} = this.getProperties('username', 'password');
this.get('session').authenticate('authenticator:oauth2', username, password).catch((reason) => {
this.set('errorMessage', reason.error || reason);
});
}
It was my mistake.

Resources