Using the Swagger module provided from NestJS there is no functionality to implement an OAuth connection that automatically get the user token through implicit flows.
From the website there is a lack of documentation about oauth.
https://docs.nestjs.com/openapi/security.
The solution that I've found is using this piece of code
const config = new DocumentBuilder()
.setTitle('Swagger API')
.setVersion('1.0')
.addSecurity('ApiKeyAuth', {
type: 'apiKey',
in: 'header',
name: 'token',
})
.addBearerAuth()
.addOAuth2(
{
type: 'oauth2',
flows: {
implicit: {
tokenUrl: `${configSv.get("OAUTH_DOMAIN")}/oauth/token`,
authorizationUrl: `${configSv.get("OAUTH_DOMAIN")}/authorize`,
scopes: {"read:products": null, "read:properties": null, "read:categories": null, openid: null, profile: null, email: null},
},
},
},
)
.build();
But doesn't retrieve correctly the token and scopes aren't working, facing a 403 error calling the endpoints
Related
I am using Next Auth to authenticate through Azure Active Directory. I am successfully able to do so but the profile object does not contain some info I need.
I am trying to get the "user type" and "account status" properties.
Here's my code
providers: [
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
userinfo: {
url: 'https://graph.microsoft.com/v1.0/me/',
params: {
scope: 'https://graph.microsoft.com/user.read',
grant_type: 'authorization_code'
},
},
})
]
I don't know what to do after this point or even if this is what I should do. Any help is appreciated.
UPDATE:
Here's what I have after changing to what was suggested
providers: [
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
userinfo: {
url: 'https://graph.microsoft.com/v1.0/me?$select=accountEnabled,userType,displayName,givenName,objectId,email,surname',
params: {
scope: 'https://graph.microsoft.com/user.read',
grant_type: 'authorization_code',
},
},
profile(profile) {
return {
id: profile.objectId,
name: profile.displayName,
lastName: profile.surname,
firstName: profile.givenName,
email: profile.email,
userType: profile.userType,
accountStatus: profile.accountEnabled
};
}
})]
It seems like the profile data from the AzureADProvider is still being used because of the id token. I thought userinfo would overwrite it but it doesn't seem to work that way unless I am doing it wrong.
I tried to reproduce the same in my environment and got the results like below:
I created Azure AD Application and granted API permissions:
I generated the Access Token using Authorization Code Flow by using parameters like below:
GET https://login.microsoftonline.com/TenantId/oauth2/v2.0/token
client_id:ClientID
client_secret:ClientSecret
scope:https://graph.microsoft.com/user.read
grant_type:authorization_code
redirect_uri:RedirectUri
code:code
When I ran the same query as you, I dint get the userType and account status properties like below:
GET https://graph.microsoft.com/v1.0/me
Note that : By default only businessPhones, displayName, givenName, id, jobTitle, mail, mobilePhone, officeLocation, preferredLanguage, surname, userPrincipalName properties will be returned.
To get the additional user properties, make use of $select like below:
GET https://graph.microsoft.com/v1.0/me?$select=accountEnabled,userType
Modify the code like below:
providers: [
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
userinfo: {
url: 'https://graph.microsoft.com/v1.0/me?$select=accountEnabled,userType',
params: {
scope: 'https://graph.microsoft.com/user.read',
grant_type: 'authorization_code'
},
},
})
]
I found a solution. I had to use the request function inside userinfo and fetch the profile data.
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
userinfo: {
url: 'https://graph.microsoft.com/v1.0/me?$select=id,userPrincipalName,accountEnabled,userType,givenName,surname',
async request(context) {
const response = await axios.get('https://graph.microsoft.com/v1.0/me?$select=id,userPrincipalName,accountEnabled,userType,givenName,surname',
{
headers: {
'Authorization': `Bearer ${context.tokens.access_token}`
}
}
)
const newProfile = await response.data
return {
id: newProfile.id,
email: newProfile.userPrincipalName,
firstName: newProfile.givenName,
lastName: newProfile.surname,
userType: newProfile.userType,
accountStatus: newProfile.accountEnabled
};
}
},
profile(userinfo) {
console.log(userinfo)
return {
id: userinfo.id,
email: userinfo.userPrincipalName,
firstName: userinfo.givenName,
lastName: userinfo.surname,
userType: userinfo.userType,
accountStatus: userinfo.accountEnabled
};
}
}),
I got access token and I will pass access_token to userinfo endpoint it throwing an invalid token provided error How to fix this Issue. I tried to debug why this error is throwing we have validateAccessToken method(userinfo.js) in this method check the access_token is exist or not exist using this code await ctx.oidc.provider.AccessToken.find(accessTokenValue); when i print this result it's showing undefined
oidc_configuration.js
const oidc = new Provider('http://localhost:3000', {
clients: [
{
client_id: 'oidcCLIENT',
client_secret: '...',
grant_types: ['refresh_token', 'authorization_code'],
redirect_uris: ['http://sso-client.dev/providers/7/open_id', 'http://sso-client.dev/providers/8/open_id'],
}
],
interactions: {
url(ctx, interaction) { // eslint-disable-line no-unused-vars
return `/api/v1/open_id/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: {
devInteractions: { enabled: false }, // defaults to true
deviceFlow: { enabled: true }, // defaults to false
revocation: { enabled: true }, // defaults to false
},
jwks: {
keys: [
{
d: 'VEZOsY07JTFzGTqv6cC2Y32vsfChind2I_TTuvV225_-0zrSej3XLRg8iE_u0-3GSgiGi4WImmTwmEgLo4Qp3uEcxCYbt4NMJC7fwT2i3dfRZjtZ4yJwFl0SIj8TgfQ8ptwZbFZUlcHGXZIr4nL8GXyQT0CK8wy4COfmymHrrUoyfZA154ql_OsoiupSUCRcKVvZj2JHL2KILsq_sh_l7g2dqAN8D7jYfJ58MkqlknBMa2-zi5I0-1JUOwztVNml_zGrp27UbEU60RqV3GHjoqwI6m01U7K0a8Q_SQAKYGqgepbAYOA-P4_TLl5KC4-WWBZu_rVfwgSENwWNEhw8oQ',
dp: 'E1Y-SN4bQqX7kP-bNgZ_gEv-pixJ5F_EGocHKfS56jtzRqQdTurrk4jIVpI-ZITA88lWAHxjD-OaoJUh9Jupd_lwD5Si80PyVxOMI2xaGQiF0lbKJfD38Sh8frRpgelZVaK_gm834B6SLfxKdNsP04DsJqGKktODF_fZeaGFPH0',
dq: 'F90JPxevQYOlAgEH0TUt1-3_hyxY6cfPRU2HQBaahyWrtCWpaOzenKZnvGFZdg-BuLVKjCchq3G_70OLE-XDP_ol0UTJmDTT-WyuJQdEMpt_WFF9yJGoeIu8yohfeLatU-67ukjghJ0s9CBzNE_LrGEV6Cup3FXywpSYZAV3iqc',
e: 'AQAB',
kty: 'RSA',
n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ',
p: '5wC6nY6Ev5FqcLPCqn9fC6R9KUuBej6NaAVOKW7GXiOJAq2WrileGKfMc9kIny20zW3uWkRLm-O-3Yzze1zFpxmqvsvCxZ5ERVZ6leiNXSu3tez71ZZwp0O9gys4knjrI-9w46l_vFuRtjL6XEeFfHEZFaNJpz-lcnb3w0okrbM',
q: '3I1qeEDslZFB8iNfpKAdWtz_Wzm6-jayT_V6aIvhvMj5mnU-Xpj75zLPQSGa9wunMlOoZW9w1wDO1FVuDhwzeOJaTm-Ds0MezeC4U6nVGyyDHb4CUA3ml2tzt4yLrqGYMT7XbADSvuWYADHw79OFjEi4T3s3tJymhaBvy1ulv8M',
qi: 'wSbXte9PcPtr788e713KHQ4waE26CzoXx-JNOgN0iqJMN6C4_XJEX-cSvCZDf4rh7xpXN6SGLVd5ibIyDJi7bbi5EQ5AXjazPbLBjRthcGXsIuZ3AtQyR0CEWNSdM7EyM5TRdyZQ9kftfz9nI03guW3iKKASETqX2vh0Z8XRjyU',
use: 'sig',
}, {
crv: 'P-256',
d: 'K9xfPv773dZR22TVUB80xouzdF7qCg5cWjPjkHyv7Ws',
kty: 'EC',
use: 'sig',
x: 'FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4',
y: '_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4',
},
],
},
});
// Heroku has a proxy in front that terminates ssl, you should trust the proxy.
oidc.proxy = true;
const callback = oidc.callback();
How to fix this issue
You're running without a persistent adapter, meaning an in-memory one is used, are you possibly restarting your server after receiving the access token before calling the userinfo endpoint?
After completing the authentication, you will get access_token as well as id_token. I think you were using id_token in place of access_token which is why you are seeing that error
Instead use access_token then you can see the details
Can someone tell me if it's possible to create a userPool with MFA required, SMS MFA disabled but OTP MFA enabled.
On the AWS console, it doesn't seem to be an issue but for some reason through CDK, it isn't quite happy.
I have attempted to update an existing user pool as well as create one new.
new UserPool(this, 'foo-user-pool', {
userPoolName: 'foo',
selfSignUpEnabled: false,
passwordPolicy: {
minLength: 12,
requireDigits: true,
requireLowercase: true,
requireSymbols: true,
requireUppercase: true,
tempPasswordValidity: cdk.Duration.days(7)
},
accountRecovery: AccountRecovery.EMAIL_ONLY,
enableSmsRole: false,
mfa: Mfa.REQUIRED,
mfaSecondFactor: {
sms: false,
otp: true
},
signInAliases: {
email: true,
},
autoVerify: {
email: true,
},
})
Gives me the following error message and has been for some time now...
SMS configuration and Auto verification for phone_number are required when MFA is required/optional (Service: AWSCognitoIdentityProviderService; Status Code: 400; Error Code: InvalidParameterException; Request ID: bcc2143c-546a-439a-b7b5-6fcf3888cf9a; Proxy: null)
The link shows a PR that allows us to disabled the SMS role creation.
AWS CDK 1.77.0
I am using Amazon Cognito user pool and AdminCreateUser api to create a new user so that we don't allow users to sign themselves up. It works great but it seems that email verification step is being skipped so when making an api call, I needed to set email_verified attribute to true to make reset password flow to work.
Can I make email verification to happen before sending out an inviatation email?
const params = {
DesiredDeliveryMediums: ['EMAIL'],
UserAttributes: [
{
Name: 'email',
Value: email
},
{
Name: 'email_verified',
Value: 'True'
},
],
Username: email,
UserPoolId: userPoolId,
}
cognitoIdentityService.adminCreateUser(params, function(err, data) {
// ...
To prevent backend to send verification email, set MessageACtion="SUPPRESS"
response = client.admin_create_user(
UserPoolId='USER_POOL_ID',
Username='USERNAME',
TemporaryPassword='PASSWORD',
UserAttributes=[
{
'Name': 'email',
'Value': 'email#example.com'
},
{
'Name': 'email_verified',
'Value': 'true'
}
],
MessageAction='SUPPRESS'
)
I am building a android client for my app which already has a web client and uses loopback-component-passport for FB authentication.
I am able to successfully get the access_token from FB through my Android App.
I figured out that the default passport component with loopback works by doing a GET to the callback url with Authorization Code which then gets access_token from FB. Since I already have the access_token, how can I circumvent this Authorization Code step so that I can handle the remaining logic of user creation using passport only otherwise I will have two different codes handling FB login.
The current version of passport.js doesn't have anything which will allow one to simply integrate Android Native FB login with loopback+passport.
The reason its difficult to integrate is because when one authenticates using web, the loopback endpoint of FB callback receives a authentication token which it then uses to create a access_token while in the case of native FB login through FB app, one directly gets the access_token.
The steps to integrate are as follows:
Setup your user model. Lets assume that you extend the standard User model from Loopback models.
Setup a seperate model which will store credentials that we will get
from FB i.e. access_token, ttl etc. Lets call it FacebookAccessToken. Also setup the relationship with user model. user.hasOne(FacebookAccessToken) and FacebookAccessToken.belongTo(user)
{
"name": "FacebookAccessToken",
"base": "PersistedModel",
"idInjection": false,
"options": {
"validateUpsert": true
},
"properties": {
"FbUserId": {
"type": "string",
"id": true,
"required": true
},
"token": {
"type": "string",
"required": true
},
"expires": {
"type": "date",
"required": true
},
"userId": {
"type": "string",
"required": true
}
},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "userId"
}
},
"acls": [],
"methods": []
}
Create a custom remote method in your user model ( say loginWithFB ) which will take access_token as a parameter. Use this access token to fetch required info from facebook to create a new instance of user and save the creds in UserCredential. The following code will be in user.js.
NOTE - You have to set a dummy password for each user. I would suggest you to set this to something cryptic so that any one doesn't break into your system(don't forget to add some salt). Also if you are not allowing simple username password login, disable the /login endpoint
User.loginWithAccessToken = function (accessToken,cb) {
FB.setAccessToken(accessToken);
FB.api('me', function (res) {
if(!res || res.error) {
console.log(!res ? 'error occurred' : res.error);
var err = new Error('Invalid Access Token');
err.statusCode = 401;
cb(err);
return;
}
// accessToken is valid, so
var query = { email : res.email};
User.findOne({where:query}, function (err,user){
var defaultError = new Error('login failed');
defaultError.statusCode = 401;
defaultError.code = 'LOGIN_FAILED';
if(err){
cb(defaultError);
}else if(!user){
// User email not found in the db case, create a new profile and then log him in
User.create({email: query.email, password: DUMMY_PASS}, function(err, user) {
if(err){
cb(defaultError);
}else{
User.login({ email: query.email, password: DUMMY_PASS}, function(err,accessToken){
cb(null,accessToken);
});
}
});
}
else{
// User found in the database, so just log him in
User.login({ email: query.email, password: DUMMY_PASS}, function(err,accessToken){
if(err){
cb(defaultError);
}else{
cb(null,accessToken);
}
});
}
});
});
};
User.remoteMethod(
'loginWithAccessToken',
{
description: 'Logins a user by authenticating it with an external entity',
accepts: [
{ arg: 'external_access_token', type: 'string', required: true, http: { source:'form'} }
],
returns: {
arg: 'accessToken', type: 'object', root: true,
description:
'The response body contains properties of the AccessToken created on login.\n' +
'Depending on the value of `include` parameter, the body may contain ' +
'additional properties:\n\n' +
' - `user` - `{User}` - Data of the currently logged in user. (`include=user`)\n\n'
},
http: {verb: 'post'}
}
);
you can install passport-facebook-token and add it as an entry to your providers.json like:
"facebook-token": {
"provider": "facebook-mobile",
"module": "passport-facebook-token",
"clientID": "CLIENT_ID",
"clientSecret": "CLIENT_SECRET",
"callbackPath": "/auth/facebook/token",
"failureFlash": true,
"strategy": "FBTokenMobileStrategy",
"json": true,
"session": false
}
then you can make a call to /auth/facebook/token?access_token=PASS_YOUR_ACCESS_TOKEN_HERE and it will return json with the user's accesstoken from loopback and the user's userId
this will still create the UserIdentities, etc. without any additional configuration
note the "strategy" can really be any random string, because the passport-facebook-token now doesn't need the .Strategy but it's necessary to enter something here in options so that loopback-component-passport defaults to requiring the module without the .Strategy appended