Unit tests on methods/publication that requires authentication in meteor - unit-testing

I'm writing unit test for my Meteor 1.4.2 application, where few of my methods requires authentication before processing.
How should I test these methods?
So far, I've written a test with practicalmeteor:mocha to create a new user and login with that user.
describe('login method', function () {
let logingKey;
beforeEach(function () {
Meteor.users.remove({});
const createUser = Meteor.server.method_handlers['registerUser'];
let params = {
username: 'testUsername'
}
res = createUser.apply({}, [params]);
logingKey = res.key;
});
it('can provide authentication', function () {
const loginUser = Meteor.server.method_handlers['login'];
let params = {
key: logingKey
}
console.log(params);
loginUser.apply({}, [params]);
});
I've written a custom login handler to login with the generated key which works fine with application, but in test results I'm getting following error.
Error: Cannot read property 'id' of undefined
at AccountsServer.Ap._setLoginToken (packages/accounts-base/accounts_server.js:889:35)
at packages/accounts-base/accounts_server.js:288:10
at Object.Meteor._noYieldsAllowed (packages/meteor.js:671:12)
at AccountsServer.Ap._loginUser (packages/accounts-base/accounts_server.js:287:10)
at AccountsServer.Ap._attemptLogin (packages/accounts-base/accounts_server.js:349:12)
at Object.methods.login (packages/accounts-base/accounts_server.js:533:21)
at Object.methodMap.(anonymous function) (packages/meteorhacks_kadira.js:2731:30)
at Test.<anonymous> (imports/api/methods/loginUser.tests.js:30:17)
at run (packages/practicalmeteor:mocha-core/server.js:34:29)
at Context.wrappedFunction (packages/practicalmeteor:mocha-core/server.js:63:33)
What could be wrong here? any suggestions are welcome, thanks in advance.
Original post on meteor forum
UPDATE
Ok! here is my confustion, Let say I've a write a unit test for this method, How should I verify or get the userId here.
Meteor.methods({
userStatus:function(update){
check(update, {online: String})
if (! this.userId) {
throw new Meteor.Error('error-not-authorized','User need to login', {method: "userStatus"})
}
try {
Meteor.users.update(Meteor.userId(),{$set: {'status.online': !!parseInt(update.online)}})
} catch (e) {
console.error("Error",e);
}
}
});

You are directly invoking a method handler without an appropriate context (which should be a Method Invocation object, while you provide an empty object). The login method handler attempts to get the connection id and fails to do so.
If you want to test the integration of your package with the accounts-base package (and basically you do, as you are calling some of its code), you can create a connection and call the method with that connection.
let connection = DDP.connect(Meteor.absoluteUrl());
// prepare the login data
const params = {/*...*/};
connection.call('login', params);
// test post conditions
connection.disconnect();
Edit (following question edit):
The answer remains pretty much the same. Once you have called the login method and logged in the user, the connection state on the server should include the logged-in user's id. Now you can call the methods that require the user to be logged in.
Note that you should probably use this.userId on all occasions (and not Meteor.userId()).

Related

Use Google Cloud Secret Manager to fetch AUTH0_CLIENT_SECRET and use with nextjs-auth0

I have a Next.js application where authentication is set up with the Auth0 Next.js SDK.
Currently the AUTH0_CLIENT_SECRET is being set as an environment variable when deploying.
I would like to use Google Cloud Secret Manager to get the AUTH0_CLIENT_SECRET during runtime and set it using the initAuth0 method.
I'm following this example: https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#create-your-own-instance-of-the-sdk
But I can't figure out how I can await the response from secret manager when I need to have the secret ready for calling the method initAuth0({clientSecret...}) and I need that in place to setup the auth end points with auth0.handleAuth().
This is my attempt: /pages/api/auth/[...auth].ts
import { initAuth0 } from "#auth0/nextjs-auth0";
const asyncHandleAuth = async () => {
const clientSecret = await getSecret("AUTH0_CLIENT_SECRET");
const auth0 = initAuth0({
clientSecret // The rest of the config is set with environment variables
});
return auth0.handleAuth();
};
export default asyncHandleAuth();
After some hair pulling I found the problem. Next.js expects the export default function to be of type NextApiHandler but I was returning Promise<NextApiHandler>.
I solved it by wrapping it in another function that takes the request and response arguments and use them to call handleAuth before returning it.
This worked for me:
const asyncHandleAuth =
() => async (req: NextApiRequest, res: NextApiResponse) => {
const clientSecret = await getSecret("AUTH0_CLIENT_SECRET");
const auth0 = initAuth0({
clientSecret, // The rest of the config is set with environment variables
});
return auth0.handleAuth()(req, res);
};
export default asyncHandleAuth();
In the code you posted in your answer:
const clientSecret = await getSecret("AUTH0_CLIENT_SECRET");
you are already waiting until the secret is returned: your code will suspend on that line until getSecret ends. As a consequence, the secret should be ready when using the initAuth0 function.
Perhaps, and according to your comments, the problem could be motivated by your export. You are exporting the asyncHandleAuth function like this:
export default asyncHandleAuth();
But I think it should be instead:
export default asyncHandleAuth;
Your answer makes perfect sense: the actual problem is that you need to provide the appropriate arguments, the request and response representations, to your handler function to perform the actual invocation. But please, be aware that the proposed export default still is valid, in your code you are executing a function that returns the thing that is being exported. Probably you could simplify it like this:
import { initAuth0 } from "#auth0/nextjs-auth0";
const asyncHandleAuth = async (req: NextApiRequest, res: NextApiResponse) => {
const clientSecret = await getSecret("AUTH0_CLIENT_SECRET");
const auth0 = initAuth0({
clientSecret // The rest of the config is set with environment variables
});
return auth0.handleAuth()(req, res);
};
export default asyncHandleAuth;
Note that there is no need for the first arrow function.

How to run set of request before every request in Postman

I'm writing test in Postman and I have multiple requests grouped like this:
Some test title:
Create a user (set of "pre-requests"):
sending a few requests required to create a user, necessary to run tests
Some action on a created user (here I'm testing what is in the test title)
one or more requests
Another test title:
Create a user (set of "pre-requests", the same as in test 1.):
sending a few requests required to create a user, necessary to run tests
Some action on a created user (not related ot test 1.)
one or more requests
To summarize I need to create a user before every request when I want to test something.
My question - How can I re-use "Create a user" set of requests without copying it?
This is how you can re-use the set of requests in pre-testcases and Test by setting environment variable and call it using eval function
pre-test case -
var Create_a_user = () => {
pm.sendRequest("http://mocktarget.apigee.net/json", function(err, res) {
tests["Status code is 200"] = pm.expect(res).to.have.property('code', 200);
console.log('firstName',res.json().firstName);
});
pm.sendRequest("http://mocktarget.apigee.net/json", function(err, res) {
tests["Status code is 200"] = pm.expect(res).to.have.property('code', 200);
console.log("lastName - "+ res.json().lastName);
});
pm.sendRequest("http://mocktarget.apigee.net/json", function(err, res) {
tests["Status code is 200"] = res.code === 200;
console.log("city - "+ res.json().lastName.city);
});
};
pm.environment.set("Create_a_user", Create_a_user.toString());
Test -
eval(pm.environment.get("Create_a_user"))();
output -
firstName - John
lastName - Doe
city - San Jose
Disclaimer -
Use Eval function carefully, it may fizzled up your code or its execution.
If i'm understanding correctly what you want is unfortunately not currently supported. I've been waiting on this feature a while myself.
https://github.com/postmanlabs/postman-app-support/issues/1535

AWS Cognito integration swift3 Refresh provides ResourceNotFoundException

Following the answer here:
https://github.com/aws/aws-sdk-ios/issues/357
At the very bottom there is a mini guide on getting swift and cognito working.
I've made a AWSCustomIdentityProvider as such:
import Foundation
import AWSCognitoIdentityProvider
import AWSCognito
class AWSCustomIdentityProvider: NSObject, AWSIdentityProviderManager
{
private var dict = NSDictionary()
func addToken(value:NSString)
{
dict = ["graph.facebook.com":value]
}
public func logins() -> AWSTask<NSDictionary>
{
return AWSTask(result: dict)
}
}
And I have a login method from facebook:
public func loginButtonDidCompleteLogin(_ loginButton: FacebookLogin.LoginButton, result: FacebookLogin.LoginResult){
switch result {
case .failed(let error):
print("FACEBOOK LOGIN FAILED: \(error)")
case .cancelled:
print("User cancelled login.")
case .success(_, _, let accessToken):
let customIdentity = AWSCustomIdentityProvider()
let token = accessToken.authenticationToken
customIdentity.addToken(value: token as NSString)
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: REGIONTYPE, identityPoolId: "XXXXXXXXXXXXXXXXXXXXXXX", identityProviderManager:customIdentity)
credentialsProvider.clearKeychain()
credentialsProvider.clearCredentials()
let serviceConfiguration = AWSServiceConfiguration(region: REDIONTYPE, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = serviceConfiguration;
credentialsProvider.getIdentityId().continue( { (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("Error: " + (task.error?.localizedDescription)!)// gets called
}
else {
print(task.result)//identityid
}
return nil
})
}
}
However I get the error:
Error Domain=com.amazonaws.AWSCognitoIdentityErrorDomain Code=8 "(null)" UserInfo={__type=NotAuthorizedException, message=Logins don't match. Please include at least one valid login for this identity or identity pool.}
Please let me know if you have any idea on how to resolve my issue. I've tried also following the docs and setting the logins directly "credentialsProvider.logins = {"graph.facebook.com": mytoken}
and that produces a different exception upon invoking a lambda method but DOES retrieve the identityID properly. However doing it according to the docs makes a warning that the method I'm using is deprecated.
The error I get:
UserInfo={NSLocalizedDescription=serialized object is neither a valid json Object nor NSData object: }
However that only happens sometimes. If I retry then i can potentially get the identity id but upon invoking a lambda method, I get the same error. I'm assuming it is a cognito issue.
UPDATE
If I use AWSCognitoLoginProviderKey.facebook.rawValue instead of graph.facebook.com in the first part, then it gives me the cognito ID and then I invoke the lambda method. I'll include the lambda method just incase that's the part I'm getting wrong but I'm decently sure that it's cognito that is preventing me from calling the lambda method:
import AWSLambda
import Foundation
struct AWSHelper{
let lambda = AWSLambda.default()
let APPLICATION_NAME = "MYAPPLICATION"
init(){
}
func getFunctionName(funcName: String) -> String{
return "\(funcName)_\(APPLICATION_NAME)"
}
func login(facebookID: String, cognitoID:String, callback:#escaping (Bool) -> Void){
let req = AWSLambdaInvocationRequest();
req?.invocationType = AWSLambdaInvocationType.requestResponse
req?.payload = ["cognitoID" : cognitoID, "facebookID" : facebookID]
req?.functionName = getFunctionName(funcName: "MYFUNCNAME")
lambda.invoke(req!) { (response: AWSLambdaInvocationResponse?,error: Error?) in
print(error)
let payload = response?.payload
print(payload)
callback(true)
}
}
}
Update 2
I have found out that calling a refresh method like this:
credentialsProvider.credentials().continue({ (task: AWSTask!) -> Any? in
print(task.result)
})
Causes an error like this:
AWSiOSSDK v2.4.10 [Error] AWSCredentialsProvider.m line:577 | __44-[AWSCognitoCredentialsProvider credentials]_block_invoke.352 | Unable to refresh. Error is [Error Domain=com.amazonaws.AWSCognitoIdentityErrorDomain Code=10 "(null)" UserInfo={__type=ResourceNotFoundException, message=Identity 'us-east-1:0db18266-1baa-4c59-9110-f9041dc92ead' not found.}]
I believe the big string that looks like an identitypoolID is actually the identityID for the given user that I have, so cognito has distributed an ID but is not able to query it?
the error:
Logins don't match. Please include at least one valid login for this identity or identity pool
Can also occur because you attempt to log in as another user without logging out, so the token in the logins dictionary is compared to the identityId for a different identity (and doesn't match). In this case the SDK usually recovers by retrying, clearing and reestablishing the identityId, and then it works.
But in your case since you are constructing your own logins dictionary, the issue is more likely that you have constructed a token that does not match. You can inspect tokens using https://jwt.io. (though I admit it works for google and cognito user pools, but not on facebook tokens (I don't know why this is)),
I think doesn't match means that the identityId records a different unique user than is specified in the token.
Are you sure the token is constructed correctly?
As you mentioned... the documentation.. well .. I find the documentation is not worth looking at, So I set up my projects so I can review working code and set breakpoints.
Here is a snippet of the code from Mobile Hub Helper's Facebook AWSSignInProvider, which shows what they use to get the token (token.tokenstring).
- (AWSTask<NSString *> *)token {
FBSDKAccessToken *token = [FBSDKAccessToken currentAccessToken];
NSString *tokenString = token.tokenString;
NSDate *idTokenExpirationDate = token.expirationDate;
if (tokenString
// If the cached token expires within 10 min, tries refreshing a token.
&& [idTokenExpirationDate compare:[NSDate dateWithTimeIntervalSinceNow:AWSFacebookSignInProviderTokenRefreshBuffer]] == NSOrderedDescending) {
return [AWSTask taskWithResult:tokenString];
}
AWSTaskCompletionSource *taskCompletionSource = [AWSTaskCompletionSource taskCompletionSource];
[FBSDKLoginManager renewSystemCredentials:^(ACAccountCredentialRenewResult result, NSError *error) {
if (result == ACAccountCredentialRenewResultRenewed) {
FBSDKAccessToken *token = [FBSDKAccessToken currentAccessToken];
NSString *tokenString = token.tokenString;
taskCompletionSource.result = tokenString;
} else {
taskCompletionSource.error = error;
}
}];
return taskCompletionSource.task;
}
Also... It bears mentioning. The AWSIdentityManager, and it's associated AWSSignInProviders is a nice architecture for getting signed in with Facebook and Google. Even if you don't use the rest of Mobile Hub Helper. Why re-invent the wheel, they did a very good job on the Identity portion of aws-mobilehub-helper-ios
I have a version of that library posted on github that adds an AWSSignInProvider for Cognito User Pools as well. SignIn-awsmhh it requires some fixes in the aws-mobilehub-helper-ios to use cognito user pools they are here aws-mobilehub-helper-ios (so if you clone do a clone --recursive and you will be set up for debugging using breakpoints in the library).
Few things that made it work I think.
I made the correct move by making my own identityprovidermanager and I think the main thing that was blocking me from executing a lambda method was actually the fact that I was using AWSLambda instead of AWSLambdaInvoker. After I switched it started making errors that made sense.

Identity Server 3 Facebook Login Get Email

Identity server is implemented and working well. Google login is working and is returning several claims including email.
Facebook login is working, and my app is live and requests email permissions when a new user logs in.
The problem is that I can't get the email back from the oauth endpoint and I can't seem to find the access_token to manually request user information. All I have is a "code" returned from the facebook login endpoint.
Here's the IdentityServer setup.
var fb = new FacebookAuthenticationOptions
{
AuthenticationType = "Facebook",
SignInAsAuthenticationType = signInAsType,
AppId = ConfigurationManager.AppSettings["Facebook:AppId"],
AppSecret = ConfigurationManager.AppSettings["Facebook:AppSecret"]
};
fb.Scope.Add("email");
app.UseFacebookAuthentication(fb);
Then of course I've customized the AuthenticateLocalAsync method, but the claims I'm receiving only include name. No email claim.
Digging through the source code for identity server, I realized that there are some claims things happening to transform facebook claims, so I extended that class to debug into it and see if it was stripping out any claims, which it's not.
I also watched the http calls with fiddler, and I only see the following (apologies as code formatting doesn't work very good on urls. I tried to format the querystring params one their own lines but it didn't take)
(facebook.com)
/dialog/oauth
?response_type=code
&client_id=xxx
&redirect_uri=https%3A%2F%2Fidentity.[site].com%2Fid%2Fsignin-facebook
&scope=email
&state=xxx
(facebook.com)
/login.php
?skip_api_login=1
&api_key=xxx
&signed_next=1
&next=https%3A%2F%2Fwww.facebook.com%2Fv2.7%2Fdialog%2Foauth%3Fredirect_uri%3Dhttps%253A%252F%252Fidentity.[site].com%252Fid%252Fsignin-facebook%26state%3Dxxx%26scope%3Demail%26response_type%3Dcode%26client_id%3Dxxx%26ret%3Dlogin%26logger_id%3Dxxx&cancel_url=https%3A%2F%2Fidentity.[site].com%2Fid%2Fsignin-facebook%3Ferror%3Daccess_denied%26error_code%3D200%26error_description%3DPermissions%2Berror%26error_reason%3Duser_denied%26state%3Dxxx%23_%3D_
&display=page
&locale=en_US
&logger_id=xxx
(facebook.com)
POST /cookie/consent/?pv=1&dpr=1 HTTP/1.1
(facebook.com)
/login.php
?login_attempt=1
&next=https%3A%2F%2Fwww.facebook.com%2Fv2.7%2Fdialog%2Foauth%3Fredirect_uri%3Dhttps%253A%252F%252Fidentity.[site].com%252Fid%252Fsignin-facebook%26state%3Dxxx%26scope%3Demail%26response_type%3Dcode%26client_id%3Dxxx%26ret%3Dlogin%26logger_id%3Dxxx
&lwv=100
(facebook.com)
/v2.7/dialog/oauth
?redirect_uri=https%3A%2F%2Fidentity.[site].com%2Fid%2Fsignin-facebook
&state=xxx
&scope=email
&response_type=code
&client_id=xxx
&ret=login
&logger_id=xxx
&hash=xxx
(identity server)
/id/signin-facebook
?code=xxx
&state=xxx
I saw the code parameter on that last call and thought that maybe I could use the code there to get the access_token from the facebook API https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow
However when I tried that I get a message from the API telling me the code has already been used.
I also tried to change the UserInformationEndpoint to the FacebookAuthenticationOptions to force it to ask for the email by appending ?fields=email to the end of the default endpoint location, but that causes identity server to spit out the error "There was an error logging into the external provider. The error message is: access_denied".
I might be able to fix this all if I can change the middleware to send the request with response_type=id_token but I can't figure out how to do that or how to extract that access token when it gets returned in the first place to be able to use the Facebook C# sdk.
So I guess any help or direction at all would be awesome. I've spent countless hours researching and trying to solve the problem. All I need to do is get the email address of the logged-in user via IdentityServer3. Doesn't sound so hard and yet I'm stuck.
I finally figured this out. The answer has something to do with Mitra's comments although neither of those answers quite seemed to fit the bill, so I'm putting another one here. First, you need to request the access_token, not code (authorization code) from Facebook's Authentication endpoint. To do that, set it up like this
var fb = new FacebookAuthenticationOptions
{
AuthenticationType = "Facebook",
SignInAsAuthenticationType = signInAsType,
AppId = ConfigurationManager.AppSettings["Facebook:AppId"],
AppSecret = ConfigurationManager.AppSettings["Facebook:AppSecret"],
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, ClaimValueTypes.String, "Facebook"));
return Task.FromResult(0);
}
}
};
fb.Scope.Add("email");
app.UseFacebookAuthentication(fb);
Then, you need to catch the response once it's logged in. I'm using the following file from the IdentityServer3 Samples Repository, which overrides (read, provides functionality) for the methods necessary to log a user in from external sites. From this response, I'm using the C# Facebook SDK with the newly returned access_token claim in the ExternalAuthenticationContext to request the fields I need and add them to the list of claims. Then I can use that information to create/log in the user.
public override async Task AuthenticateExternalAsync(ExternalAuthenticationContext ctx)
{
var externalUser = ctx.ExternalIdentity;
var claimsList = ctx.ExternalIdentity.Claims.ToList();
if (externalUser.Provider == "Facebook")
{
var extraClaims = GetAdditionalFacebookClaims(externalUser.Claims.First(claim => claim.Type == "urn:facebook:access_token"));
claimsList.Add(new Claim("email", extraClaims.First(k => k.Key == "email").Value.ToString()));
claimsList.Add(new Claim("given_name", extraClaims.First(k => k.Key == "first_name").Value.ToString()));
claimsList.Add(new Claim("family_name", extraClaims.First(k => k.Key == "last_name").Value.ToString()));
}
if (externalUser == null)
{
throw new ArgumentNullException("externalUser");
}
var user = await userManager.FindAsync(new Microsoft.AspNet.Identity.UserLoginInfo(externalUser.Provider, externalUser.ProviderId));
if (user == null)
{
ctx.AuthenticateResult = await ProcessNewExternalAccountAsync(externalUser.Provider, externalUser.ProviderId, claimsList);
}
else
{
ctx.AuthenticateResult = await ProcessExistingExternalAccountAsync(user.Id, externalUser.Provider, externalUser.ProviderId, claimsList);
}
}
And that's it! If you have any suggestions for simplifying this process, please let me know. I was going to modify this code to do perform the call to the API from FacebookAuthenticationOptions, but the Events property no longer exists apparently.
Edit: the GetAdditionalFacebookClaims method is simply a method that creates a new FacebookClient given the access token that was pulled out and queries the Facebook API for the other user claims you need. For example, my method looks like this:
protected static JsonObject GetAdditionalFacebookClaims(Claim accessToken)
{
var fb = new FacebookClient(accessToken.Value);
return fb.Get("me", new {fields = new[] {"email", "first_name", "last_name"}}) as JsonObject;
}

Meteor share sessions data between client and server

I'm building a restricted signup. I want user with a specific code passed in a url to be able to signup and not others. I'm using the accounts package.
I can prevent account creation in the Accounts.onCreateUser method. I'm looking for a way to tell the server if the client had an authorised signup code. With a classic form (email+password) I can just add an extra hidden field. How can I achieve the same result if the user signs up with let's say Facebook?
Since Meteor doesn't use cookies, I can't store this info in a cookie that the server would access. Session variable are not accessible server side. And since I'm not controlling what got send with the account-facebook creation, I can't use a Session variable on the client side that I'd pass along when the user presses sign up.
Any idea"?
Just add the special token to the user object being passed to Accounts.createUser():
var user = {
email: email,
password: password,
profile: {
token: token
}
};
Accounts.createUser(user, function (error, result) {
if (error) {
console.log(error)
}
});
On the server side you can access this in the Accounts.onCreateUser():
Accounts.onCreateUser(function(options, user) {
console.log(options);
console.log(user);
});
I think it's in the options variable that you will find your token, so it would be options.profile.token.
for me, the best option here was passing in custom parameters to loginbuttons.
see the package docs:
https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3
Where it outlines the below:
accountsUIBootstrap3.setCustomSignupOptions = function() {
return {
mxpDistinctId: Session.get('mxpdid'),
leadSource: Session.get('leadSource')
}
};