I want to let users authenticate via SoundCloud for my ASP.NET MVC 4 project. Since there is no .NET SDK, I wrote a custom OAuth2Client to handle the authentication. After adding the client to my AuthConfig.cs, it appropriately showed up as an option to login. The problem is, when I click on the button to login, it always returns
Login Failure.
Unsuccessful login with service.
without even asking me to login in SoundCloud. What is the problem? I implemented a very similar client for GitHub and it worked with no problems.
Here is my client:
public class SoundCloudOAuth2Client : OAuth2Client
{
private const string ENDUSERAUTHLINK = "https://soundcloud.com/connect";
private const string TOKENLINK = "https://api.soundcloud.com/oauth2/token";
private readonly string _clientID;
private readonly string _clientSecret;
public SoundCloudOAuth2Client(string clientID, string clientSecret) : base("SoundCloud")
{
if (string.IsNullOrWhiteSpace(clientID)) {
throw new ArgumentNullException("clientID");
}
if (string.IsNullOrWhiteSpace(clientSecret)) {
throw new ArgumentNullException("clientSecret");
}
_clientID = clientID;
_clientSecret = clientSecret;
}
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
StringBuilder serviceUrl = new StringBuilder();
serviceUrl.Append(ENDUSERAUTHLINK);
serviceUrl.AppendFormat("?client_id={0}", _clientID);
serviceUrl.AppendFormat("&response_type={0}", "code");
serviceUrl.AppendFormat("&scope={0}", "non-expiring");
serviceUrl.AppendFormat("&redirect_uri={0}", System.Uri.EscapeDataString(returnUrl.ToString()));
return new Uri(serviceUrl.ToString());
}
public override void RequestAuthentication(HttpContextBase context, Uri returnUrl)
{
base.RequestAuthentication(context, returnUrl);
}
protected override IDictionary<string, string> GetUserData(string accessToken)
{
IDictionary<String, String> extraData = new Dictionary<String, String>();
var webRequest = (HttpWebRequest)WebRequest.Create("https://api.soundcloud.com/me.json?oauth_token=" + accessToken);
webRequest.Method = "GET";
string response = "";
using (HttpWebResponse webResponse = HttpWebResponse)webRequest.GetResponse())
{
using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
{
response = reader.ReadToEnd();
}
}
var json = JObject.Parse(response);
string id = (string)json["id"];
string username = (string)json["username"];
string permalinkUrl = (string)json["permalink_url"];
extraData = new Dictionary<String, String>
{
{"SCAccessToken", accessToken},
{"username", username},
{"permalinkUrl", permalinkUrl},
{"id", id}
};
return extraData;
}
protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
{
StringBuilder postData = new StringBuilder();
postData.AppendFormat("client_id={0}", this._clientID);
postData.AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(returnUrl.ToString()));
postData.AppendFormat("&client_secret={0}", this._clientSecret);
postData.AppendFormat("&grant_type={0}", "authorization_code");
postData.AppendFormat("&code={0}", authorizationCode);
string response = "";
string accessToken = "";
var webRequest = (HttpWebRequest)WebRequest.Create(TOKENLINK);
webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
using (Stream s = webRequest.GetRequestStream())
{
using (StreamWriter sw = new StreamWriter(s))
sw.Write(postData.ToString());
}
using (WebResponse webResponse = webRequest.GetResponse())
{
using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
{
response = reader.ReadToEnd();
}
}
var json = JObject.Parse(response);
accessToken = (string)json["access_token"];
return accessToken;
}
public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl)
{
string code = context.Request.QueryString["code"];
string u = context.Request.Url.ToString();
if (string.IsNullOrEmpty(code))
{
return AuthenticationResult.Failed;
}
string accessToken = this.QueryAccessToken(returnPageUrl, code);
if (accessToken == null)
{
return AuthenticationResult.Failed;
}
IDictionary<string, string> userData = this.GetUserData(accessToken);
if (userData == null)
{
return AuthenticationResult.Failed;
}
string id = userData["id"];
string name;
if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name))
{
name = id;
}
return new AuthenticationResult(
isSuccessful: true, provider: "SoundCloud", providerUserId: id, userName: name, extraData: userData);
}
}
and the AuthConfig.cs:
public static void RegisterAuth()
{
OAuthWebSecurity.RegisterClient(new SoundCloudOAuth2Client(
clientID: MyValues.MyClientID,
clientSecret: MyValues.MyClientSECRET),
displayName: "SoundCloud",
extraData: null);
OAuthWebSecurity.RegisterClient(new GitHubOAuth2Client(
appId: MyValues.GITHUBAPPID,
appSecret: MyValues.GITHUBAPPSECRET), "GitHub", null);
OAuthWebSecurity.RegisterGoogleClient();
OAuthWebSecurity.RegisterYahooClient();
}
There are multiple issues to address, starting with the first function that runs:
GetServiceLoginUrl(Uri returnUrl)The
returnUrl, which is automatically created, contains ampersands, which SoundCloud does not like. You need to strip out the ampersands and ensure the “Redirect URI for Authentication” in your SoundCloud account exactly matches what is being sent (querystring and all). Here is an example of what was being sent as the returnURL by default:First step was to remove the
&__sid__value. You can strip outsidvalue and pass it as thestateparameter, just in case you ever need it. The new function looks like this:That solves part of the problem. The redirect URI in SoundlCoud now is simply
https://localhost:44301/Account/ExternalLoginCallback?__provider__=SoundCloud). But trying to authenticate will still returnfalse. The next issue to address is inAccountController.cs, specifically:because in the first line, it tries to return:
and this doesn’t run for my custom
OAuth2Client, sinceVerifyAuthenticationtakes different parameters. Fix it by detecting if it is the SoundCloud client and then use the custom VerifyAuthentication:where
After that, everything works fine and you can successfully authenticate. You can configure
GetUserDatato get whatever SoundCloud data you want to save and then save it off to your UserProfile or related table. The key part is thatSCAccessTokenbecause that is what you will need in the future to upload to their account.