39

I'm trying to create a local Java-based client that interacts with the SurveyMonkey API.

SurveyMonkey requires a long-lived access token using OAuth 2.0, which I'm not very familiar with.

I've been googling this for hours, and I think the answer is no, but I just want to be sure:

Is it possible for me to write a simple Java client that interacts with the SurveyMonkey, without setting up my own redirect server in some cloud?

I feel like having my own online service is mandatory to be able to receive the bearer tokens generated by OAuth 2.0. Is it possible that I can't have SurveyMonkey send bearer tokens directly to my client?

And if I were to set up my own custom Servlet somewhere, and use it as a redirect_uri, then the correct flow would be as follows:

  1. Java-client request bearer token from SurveyMonkey, with redirect_uri being my own custom servlet URL.
  2. SurveyMonkey sends token to my custom servlet URL.
  3. Java-client polls custom servlet URL until a token is available?

Is this correct?

Tovi7
  • 2,505
  • 3
  • 18
  • 25
  • I don't know if it is of any help, but for google auth I get my redirect url by : VerificationCodeReceiver receiver = new LocalServerReceiver(); receiver = Preconditions.checkNotNull(receiver); String redirectUri = receiver.getRedirectUri(); – c0der Jul 08 '16 at 14:25

3 Answers3

21

Not exactly, the whole point of the OAuth flow is that the user (the client you're accessing the data on behalf of) needs to give you permission to access their data.

See the authentication instructions. You need to send the user to the OAuth authorize page:

https://api.surveymonkey.net/oauth/authorize?api_key<your_key>&client_id=<your_client_id>&response_type=code&redirect_uri=<your_redirect_uri>

This will show a page to the user telling them which parts of their account you are requesting access to (ex. see their surveys, see their responses, etc). Once the user approves that by clicking "Authorize" on that page, SurveyMonkey will automatically go to whatever you set as your redirect URI (make sure the one from the url above matches with what you set in the settings for your app) with the code.

So if your redirect URL was https://example.com/surveymonkey/oauth, SurveyMonkey will redirect the user to that URL with a code:

https://example.com/surveymonkey/oauth?code=<auth_code>

You need to take that code and then exchange it for an access token by doing a POST request to https://api.surveymonkey.net/oauth/token?api_key=<your_api_key> with the following post params:

client_secret=<your_secret>
code=<auth_code_you_just_got>
redirect_uri=<same_redirect_uri_as_before>
grant_type=authorization_code

This will return an access token, you can then use that access token to access data on the user's account. You don't give the access token to the user it's for you to use to access the user's account. No need for polling or anything.

If you're just accessing your own account, you can use the access token provided in the settings page of your app. Otherwise there's no way to get an access token for a user without setting up your own redirect server (unless all the users are in the same group as you, i.e. multiple users under the same account; but I won't get into that). SurveyMonkey needs a place to send you the code once the user authorizes, you can't just request one.

General Kandalaft
  • 2,087
  • 2
  • 16
  • 25
  • Your last suggestion, regarding the settings panel: I've been trying this as well as a workaround, but it doesn't seem to work. Is it correct for me to assume that the "access token" in the "my apps" section is the bearer token I can use for a private app? – Tovi7 Jul 10 '16 at 10:37
  • 1
    @Tovi7 yes, you can use that in the bearer to access the same account of the owner of the app. That can always be used to access the account of the app owner regardless of which status the app is in (aside from disabled). – General Kandalaft Jul 10 '16 at 19:07
  • Also, just in case you're interested, there will likely be a method to get access tokens for users in your group (case: private app), or your own account (client_credentials grant_type). You can watch the docs at: https://github.com/SurveyMonkey/public_api_docs if you're interested in being notified of changes. – General Kandalaft Jul 10 '16 at 19:10
  • Thanks for this answer. Spent 2 days on this, and finally figured out the redirect uri problem. Thanks a lot ! – zookastos May 03 '18 at 18:57
  • @Nalin, did you end up using a redirect server? – carpii May 03 '18 at 20:51
  • No. Notice - redirect_uri=. "same_redirect_uri_as_before" is the key here. I used javascript first, it opend a new window and asks user to login. In that new window url - you will have a key - redirecturl. Use that value everywhere. However my problem was specific to Google oAuth2 api, but the concept remained the same. Redirect URI was not used, but was required to validate the request to google server. – zookastos May 03 '18 at 20:56
  • I don't even *see* an "access token" in my apps section... exactly where am I supposed to be looking? – Michael Feb 01 '20 at 00:48
20

Yes, it is possible to use OAuth2 without a callback URL. The RFC6749 introduces several flows. The Implicit and Authorization Code grant types require a redirect URI. However the Resource Owner Password Credentials grant type does not.

Since RFC6749, other specifications have been issued that does not require any redirect URI:

  • RFC7522: Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants
  • RFC7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants

There is another IETF draft that tries to introduce another grant type for limited devices (https://tools.ietf.org/html/draft-ietf-oauth-device-flow) which does not require any redirect URI.

In any case, if the grant types above do not fit on your needs, nothing prevent you from creating a custom grant type.

Florent Morselli
  • 11,275
  • 4
  • 28
  • 50
4

You do need to implement something that will act as the redirect_uri, which does not necessarily need to be hosted somewhere else than your client (as you say, in some cloud).

I am not very familiar with Java and Servelets, but if I assume correctly, it would be something that could handle http://localhost:some_port. In that case, the flow that you describe is correct.

I implemented the same flow successfully in C#. Here is the class that implements that flow. I hope it helps.

class OAuth2Negotiator
{
    private HttpListener _listener = null;
    private string _accessToken = null;
    private string _errorResult = null;
    private string _apiKey = null;
    private string _clientSecret = null;
    private string _redirectUri = null;

    public OAuth2Negotiator(string apiKey, string address, string clientSecret)
    {
        _apiKey = apiKey;
        _redirectUri = address.TrimEnd('/');
        _clientSecret = clientSecret;

        _listener = new HttpListener();
        _listener.Prefixes.Add(address + "/");
        _listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
    }

    public string GetToken()
    {
        var url = string.Format(@"https://api.surveymonkey.net/oauth/authorize?redirect_uri={0}&client_id=sm_sunsoftdemo&response_type=code&api_key=svtx8maxmjmqavpavdd5sg5p",
                HttpUtility.UrlEncode(@"http://localhost:60403"));
        System.Diagnostics.Process.Start(url);

        _listener.Start();
        AsyncContext.Run(() => ListenLoop(_listener));
        _listener.Stop();

        if (!string.IsNullOrEmpty(_errorResult))
            throw new Exception(_errorResult);
        return _accessToken;
    }

    private async void ListenLoop(HttpListener listener)
    {
        while (true)
        {
            var context = await listener.GetContextAsync();
            var query = context.Request.QueryString;
            if (context.Request.Url.ToString().EndsWith("favicon.ico"))
            {
                context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                context.Response.Close();
            }
            else if (query != null && query.Count > 0)
            {
                if (!string.IsNullOrEmpty(query["code"]))
                {
                    _accessToken = await SendCodeAsync(query["code"]);
                    break;
                }
                else if (!string.IsNullOrEmpty(query["error"]))
                {
                    _errorResult = string.Format("{0}: {1}", query["error"], query["error_description"]);
                    break;
                }
            }
        }
    }

    private async Task<string> SendCodeAsync(string code)
    {
        var GrantType = "authorization_code";
        //client_secret, code, redirect_uri and grant_type. The grant type must be set to “authorization_code”
        var client = new HttpClient();
        client.BaseAddress = new Uri("https://api.surveymonkey.net");
        var request = new HttpRequestMessage(HttpMethod.Post, string.Format("/oauth/token?api_key={0}", _apiKey));

        var formData = new List<KeyValuePair<string, string>>();
        formData.Add(new KeyValuePair<string, string>("client_secret", _clientSecret));
        formData.Add(new KeyValuePair<string, string>("code", code));
        formData.Add(new KeyValuePair<string, string>("redirect_uri", _redirectUri));
        formData.Add(new KeyValuePair<string, string>("grant_type", GrantType));
        formData.Add(new KeyValuePair<string, string>("client_id", "sm_sunsoftdemo"));

        request.Content = new FormUrlEncodedContent(formData);
        var response = await client.SendAsync(request);
        if (!response.IsSuccessStatusCode)
        {
            _errorResult = string.Format("Status {0}: {1}", response.StatusCode.ToString(), response.ReasonPhrase.ToString());
            return null;
        }

        var data = await response.Content.ReadAsStringAsync();
        if (data == null)
            return null;
        Dictionary<string, string> tokenInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(data);
        return(tokenInfo["access_token"]);
    }
}
manos
  • 61
  • 4
  • Im new to socket programming and working on a simple mac desktop application. Is there an example of oauth listening to a localhost in swift? – Potion Mar 27 '19 at 18:29
  • Found this: https://alexw.me/2013/03/save-url-to-pocket-with-alfred-workflow-using-oauth/ (better a late answer than never) – Cameron Stone Dec 04 '20 at 01:03