function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Andy Kallio 7Andy Kallio 7 

Token Based Authentication to Netsuite

Hi everyone,
I'm an admin that just made their first succesfull http callout. In this case, to netsuite. Authentication was acheived by setting the header with NLAuth details. I would like for my next step to be to Authenticate with OAuth, which I think Netsuite refers to as token based authentication. 

There are few examples out there that I was able to find in blogs on how to do basic authentication, but nothing on OAuth. 

So, I am just wondering if there is anybody out there who has done it that might be willing to answer some further questions. 
Tom BarrengerTom Barrenger
Did you ever manage to implement OAuth? I am in a similar position looking for some advice. 
Andy Kallio 7Andy Kallio 7
Sorry to say but no...after giving it some effort...other priorities took over/I got distracted by other things. I haven't thought about it for a while but can say quickly that Netsuite uses OAuth1 and on top of that they seem to use their own special version of OAuth1. I will try to dig up some of the things I found that were helping me.
Tom BarrengerTom Barrenger
I managed to get an OAuth connection from Salesforce to NetSuite working so I'll leave some information and resources here for anyone else still having trouble.

To generate the signature I adapted the code shared in the following forum: https://salesforce.stackexchange.com/questions/97646/oauth-signature-generation
I found these tools useful for debugging and checking where componants like my base string were incorrect:
1.       http://lti.tools/oauth/
2.       http://dinochiesa.github.io/oauth1.0a/request-builder.html
Note - These may not be secure with secret token information so tokens should be regenerated afterwards. 
Postman was also useful to test if a request was working or not: https://www.getpostman.com/downloads/

Hopefully this is useful, feel free to reach out if you would like more information.
Andy Kallio 7Andy Kallio 7

@tom

 

Would you mind sharing some of your code? I'm having a go at it but I am more of an admin than a developer. Anyway, I basically put the OAuth Playground app into my org and made a couple small modifications to it. For example, the code does not set realm and I've just hardcoded the token for now.  Here is the Authorization header that is being sent after making those modfications:

 

OAuth 
 realm="4455555_SB1", 
 oauth_consumer_key="e3cc99816174ee2655cd82______________________________", 
 oauth_token="306b2a975efe790df4__________________________________________", 
 oauth_signature_method="HMAC-SHA1", 
 oauth_version="1.0", 
 oauth_timestamp="1550552813", 
 oauth_nonce="-1491722197348548468", 
 oauth_signature="YgfQG5Z1FJR2LJusuNlABmvFNxE%3D"

All of the paramters that are specificied in netsuite documentation are there but I get 403 Invalid_Login_Attempt back from netsuite. 

 

I wonder if it has to do with the fact that the code in OAuth playground uses RFC 5849 for encoding and Netsuite says that RFC 3986. 

Tom BarrengerTom Barrenger
Hey Andy,
I didn't play around with the Playground app so I'm not sure how to help with that.
I'm happy to share all the code I think you'll need here but did you want to share an email address with me and hopefully I can help get to the bottom of your problem a little faster?

Also a handy tip is to look at the login audit trail from the NetSuite end and see how your attempt to connect is failing.

To view the login audit trail:
1. Go to the Setup drop down
2. Go to Users/Roles
3. Select View Login Audit Trail

You'll want to personalise the search and add the following columns:
1. Token-based Application Name
2. Token-based Access Token Name

This will let you see why your login is failing i.e. invalid nonce, invalid timestamp, invalid siganture. 
 
Andy Kallio 7Andy Kallio 7
Thanks for that tip. I had looked at the logs for the script and script deployment but there was nothing there...didn't know about the login audit and can now see that there is a problem with my signature. I will post my code but should probably make some changes to it first. Will try to do that soon. 
Andy Kallio 7Andy Kallio 7
This code is working for me. 
There were definitely some challenges with using OAuth Playground to do this. Basically I had to look at Netsuite's spec for the Base String more carefully because createBaseString in OAuth playground was using a different spec. 
 
global class OAuth {

	private OAuth_Service__c service;
	private String token;
	private String tokenSecret;
	private Boolean isAccess;
	private String verifier;

	private String nonce;
	private String timestamp;
	private String signature;
	private String realm;
	private String consumerKey;
	private String consumerSecret;
	private String oauthversion;

	private Map<String,String> parameters = new Map<String,String>();
	
	public String message { get; set; }
	/**
	 * Looks up service name and starts a new authorization process
	 * returns the authorization URL that the user should be redirected to
	 * If null is returned, the request failed. The message property will contain
	 * the reason.
	 */	
	public void newAuthorization(String serviceName) {

		service = [SELECT realm__c, request_token_url__c, access_token_url__c, consumer_key__c, 
						  consumer_secret__c, authorization_url__c,
						  (select token__c, secret__c, isAccess__c FROM tokens__r WHERE owner__c=:UserInfo.getUserId() ) 
						  FROM OAuth_Service__c WHERE name = :serviceName];
		
		if(service==null) {
			System.debug('Couldn\'t find Oauth Service '+serviceName);
			message = 'Service '+serviceName+' was not found in the local configuration';
		}
				
		Http h = new Http();
		HttpRequest req = new HttpRequest();
		req.setMethod('GET');
		req.setEndpoint(service.request_token_url__c);
		System.debug('Request body set to: '+req.getBody());
		realm = service.Realm__c;
		consumerKey = service.consumer_key__c;
		consumerSecret = service.consumer_secret__c;
		token = service.Tokens__r[0].isAccess__c ? service.Tokens__r[0].Token__c : '';
		tokenSecret = service.Tokens__r[0].isAccess__c ? service.Tokens__r[0].Secret__c : '';
		sign(req);
		HttpResponse res = null;
		if(serviceName=='test1234') {
			// testing
			res = new HttpResponse();
		} else {
			try{
				res = h.send(req);
				system.debug('req '+req.getHeader('Authorization'));
				system.debug('res '+res);
				system.debug('resp body '+res.getBody());
				callOutHandler.createLog(req.getEndpoint(), req.getMethod(), res.getStatusCode(), req.getHeader('Content-Type'), req.getBody(), res.getBody());
				callOutHandler.routeResponse(res.getBody(), 'netsuite');
            
			}
			catch(exception e) {
				system.debug('exception '+e);
				callOutHandler.createLog(req.getEndpoint(), req.getMethod(), res.getStatusCode(), req.getHeader('Content-Type'), req.getBody(), res.getBody());
			}
		}
		System.debug('Response from request token request: ('+res.getStatusCode()+')'+res.getBody());
		if(res.getStatusCode()>299) {
			message = 'Request failed. HTTP Code = '+res.getStatusCode()+
					  '. Message: '+res.getStatus()+'. Response Body: '+res.getBody();
		}
	}

	public List<User> getUsersOfService(String serviceName) {
		List<OAuth_Token__c> l =
			[SELECT OAuth_Service__r.name, isAccess__c, Owner__r.name FROM OAuth_Token__c 
			 WHERE OAuth_Service__r.name= :serviceName AND isAccess__c = true];
			 
		List<User> result = new List<User>();
		for(OAuth_Token__c t : l) {
			result.add(t.owner__r);
		}
		return result;
	}

		
	private void refreshParameters() {
		parameters.clear();
		parameters.put('realm',realm);
		parameters.put('oauth_consumer_key',consumerKey);
		if(token!=null) {
			parameters.put('oauth_token',token);
		}

		if(verifier!=null) {
			parameters.put('oauth_verifier',verifier);
		}
		parameters.put('oauth_signature_method','HMAC-SHA1');
		parameters.put('oauth_version','1.0');
		parameters.put('oauth_timestamp',timestamp);
		parameters.put('oauth_nonce',nonce);
	}

	private Map<String,String> getUrlParams(String value) {

		Map<String,String> res = new Map<String,String>();
		if(value==null || value=='') {
			return res;
		}
		for(String s : value.split('&')) {
			System.debug('getUrlParams: '+s);
			List<String> kv = s.split('=');
			if(kv.size()>1) {
			  // RFC 5849 section 3.4.1.3.1 and 3.4.1.3.2 specify that parameter names 
			  // and values are decoded then encoded before being sorted and concatenated
			  // Section 3.6 specifies that space must be encoded as %20 and not +
			  String encName = EncodingUtil.urlEncode(EncodingUtil.urlDecode(kv[0], 'UTF-8'), 'UTF-8').replace('+','%20');
			  String encValue = EncodingUtil.urlEncode(EncodingUtil.urlDecode(kv[1], 'UTF-8'), 'UTF-8').replace('+','%20');
			  System.debug('getUrlParams:  -> '+encName+','+encValue);
			  res.put(encName,encValue);
			}
		}
		return res;
	}

	private String createBaseString(Map<String,String> oauthParams, HttpRequest req) {
		Map<String,String> p = oauthParams.clone();
		
		p.remove('realm');
		p.remove('deploy');
		p.remove('script');

		for(string k : oauthParams.keySet()){
			system.debug('oauthParams '+k+' '+p.get(k));
		}

		if(req.getMethod().equalsIgnoreCase('post') && req.getBody()!=null && 
		   req.getHeader('Content-Type')=='application/x-www-form-urlencoded') {
		   	p.putAll(getUrlParams(req.getBody()));
		}
		String host = req.getEndpoint();
		Integer n = host.indexOf('?');
		if(n>-1) {
			p.putAll(getUrlParams(host.substring(n+1)));
			host = host.substring(0,n);
		}
		List<String> keys = new List<String>();
		keys.addAll(p.keySet());
		for(string k2 : keys) {
			system.debug('keys '+k2);
		}
		keys.sort();
		String s = keys.get(0)+'='+p.get(keys.get(0));
		for(Integer i=1;i<keys.size();i++) {
			s = s + '&' + keys.get(i)+'='+p.get(keys.get(i));
		}

		// According to OAuth spec, host string should be lowercased, but Google and LinkedIn
		// both expect that case is preserved.
		return req.getMethod().toUpperCase()+ '&' + 
			EncodingUtil.urlEncode(host, 'UTF-8') + '&' +
			EncodingUtil.urlEncode(s, 'UTF-8');
	}
	
	public void sign(HttpRequest req) {
		
		nonce = String.valueOf(Crypto.getRandomLong());
		timestamp = String.valueOf(DateTime.now().getTime()/1000);

		refreshParameters();
		
		String s = createBaseString(parameters, req);
		System.debug('Token Secret: '+tokenSecret);
		System.debug('Signature base string: '+s);
		
		Blob sig = Crypto.generateMac('HmacSHA1', Blob.valueOf(s), 
				       Blob.valueOf(consumerSecret+'&'+tokenSecret));	   
		signature = EncodingUtil.urlEncode(EncodingUtil.base64encode(sig), 'UTF-8');
		
		String header = 'OAuth ';
		for (String key : parameters.keySet()) {
			header = header + key + '="'+parameters.get(key)+'", ';
		}
		header = header + 'oauth_signature="'+signature+'"';
		System.debug('Authorization: '+header);
		req.setHeader('Authorization',header);
	}	
}


 
Michal MasrnaMichal Masrna

Hello.
Could you give me the function that calls this class?

I have realm and consumer key, consumer secret, token, Token Secret.

I don't know how I use this class.

Let me know.

Best regards.
 

Andy Kallio 7Andy Kallio 7

I started by using the simple visualforce page and controller in the OAuth Playground that you can use to call it app https://github.com/jesperfj/sfdc-oauth-playground/tree/master/OAuth/src

 

AuthPage.page
AuthController.cls

You will also find in the code posted above my own utility class called callOutHandler that I use to post responses to a custom object. 

Tom BarrengerTom Barrenger
Hey Michal,
The class that Andy posted signs a HTTP Request when you call OAuth.sign(YOUR_REQUEST). You want to create your HTTP request and set the body and headers before you call the class. An example might look like this:
Http http = new Http(); 
HttpRequest request = new HttpRequest(); 
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals'); 
request.setMethod('POST'); 
request.setHeader('Content-Type', 'application/json;charset=UTF-8'); // Set the body as a JSON object 
request.setBody('{"name":"mighty moose"}'); // This is the information you want to send

Oauth.Sign(request); // This is where you call the class to sign this request

HttpResponse response = http.send(request); // Parse the JSON response 
if (response.getStatusCode() != 201) { 
System.debug('The status code returned was not expected: ' + response.getStatusCode() + ' ' + response.getStatus());
} 
else {
System.debug(response.getBody()); 
}

 
Michal MasrnaMichal Masrna

Thanks.

But I have some issues.

User-added image

Help me.

Michal MasrnaMichal Masrna
User-added image

GET method
Michal MasrnaMichal Masrna

Hey Tom..

Let me know the reason.
 

Tom BarrengerTom Barrenger
It looks to me like you could be missing custom objects that Andy set up to contain his token information and references in his code. If you have copied and pasted Andy's code without adding these objects in you will certainly get some errors.

My suggestion is start simple and hardcode your token information at the start of the class to look something like this:
string consumerKey = YOUR_CONSUMER_KEY;
string consumerSecret = YOUR_CONSUMER_SECRET;
string token = YOUR_TOKEN;
string tokenSecret = YOUR_TOKEN_KEY;
string realm = YOUR_REALM;
string nonce = String.valueOf(Crypto.getRandomLong());            
string timestamp = String.valueOf(DateTime.now().getTime()/1000);    
string signatureMethod = 'HMAC-SHA1';
string version = '1.0';
public Map<String,String> parameters = new Map<String,String>();
Then for now don't worry about the code between lines 26 and 88, you can set up something similar once you get your connection working. Jump straight to private void refreshParameters() { and see if your code will save.
Andy Kallio 7Andy Kallio 7
I've just learned that the code I posted above only works on GET. When I try using with POST i get the invalidSignature error again. And I have learned that his is because OAuth1 spec requires parameters from the Body to be included in the signature, I think.
https://oauth.net/core/1.0a/#auth_step1
9.1.  Signature Base String
The Signature Base String is a consistent reproducible concatenation of the request elements into a single string. The string is used as an input in hashing or signing algorithms. The HMAC-SHA1 signature method provides both a standard and an example of using the Signature Base String with a signing algorithm to generate signatures. All the request parameters MUST be encoded as described in Parameter Encoding prior to constructing the Signature Base String.


9.1.1.  Normalize Request Parameters
The request parameters are collected, sorted and concatenated into a normalized string:

Parameters in the OAuth HTTP Authorization header excluding the realm parameter.
Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded).
HTTP GET parameters added to the URLs in the query part (as defined by [RFC3986] section 3).
The oauth_signature parameter MUST be excluded.

 
Tom BarrengerTom Barrenger
I think you are correct about that, it sounds to me that if the content-type is application/x-www-form-urlencoded then the base string should include the request body. In the createBaseString method you're using this should be done automatically -
private String createBaseString(Map<String,String> oauthParams, HttpRequest req) {
		Map<String,String> p = oauthParams.clone();
		
		p.remove('realm');
		p.remove('deploy');
		p.remove('script');

		for(string k : oauthParams.keySet()){
			system.debug('oauthParams '+k+' '+p.get(k));
		}

// -------- This bit here -------- //
		if(req.getMethod().equalsIgnoreCase('post') && req.getBody()!=null && 
		   req.getHeader('Content-Type')=='application/x-www-form-urlencoded') {
		   	p.putAll(getUrlParams(req.getBody()));
		}

		String host = req.getEndpoint();
		Integer n = host.indexOf('?');
		if(n>-1) {
			p.putAll(getUrlParams(host.substring(n+1)));
			host = host.substring(0,n);
		}
		List<String> keys = new List<String>();
		keys.addAll(p.keySet());
		for(string k2 : keys) {
			system.debug('keys '+k2);
		}
		keys.sort();
		String s = keys.get(0)+'='+p.get(keys.get(0));
		for(Integer i=1;i<keys.size();i++) {
			s = s + '&' + keys.get(i)+'='+p.get(keys.get(i));
		}

		// According to OAuth spec, host string should be lowercased, but Google and LinkedIn
		// both expect that case is preserved.
		return req.getMethod().toUpperCase()+ '&' + 
			EncodingUtil.urlEncode(host, 'UTF-8') + '&' +
			EncodingUtil.urlEncode(s, 'UTF-8');
}

Check that you're setting your content type and maybe test if you're entering inside that IF statement or not. 
I had similar issues around the content type so I just changed mine -
reqUnsigned.setHeader('Content-Type', 'application/json;charset=UTF-8');

And my code to create my base string looks like this -
private String createBaseString(Map<String,String> oauthParams, HttpRequest req) {
        Map<String,String> p = oauthParams.clone();
        p.remove('realm'); // Do not put the realm in the Base String
        String host = req.getEndpoint();
        Integer n = host.indexOf('?');
        if(n>-1) {
            p.putAll(getUrlParams(host.substring(n+1)));            
            host = host.substring(0,n);            
        }
        List<String> keys = new List<String>();
        keys.addAll(p.keySet());
        keys.sort();
        String s = keys.get(0)+'='+p.get(keys.get(0));
        for(Integer i=1;i<keys.size();i++) {
            s = s + '&' + keys.get(i)+'='+p.get(keys.get(i));
        }
       
        return req.getMethod().toUpperCase()+ '&' + 
            EncodingUtil.urlEncode(host, 'UTF-8') + '&' +
            EncodingUtil.urlEncode(s, 'UTF-8');
}

This works for me for POST operations, hopefully it will work for you too.
Andy Kallio 7Andy Kallio 7
Thank you Tom. Your createBaseString is better than mine....I now have it working for POST. Hopefully it will work for the other verbs too.