Asp.Net MVC OAuth Login (Github)Beğen
OAuth, açık-standart yetkilendirme anlamına gelen ve kimlik doğrulama ve yetkilendirme amacı ile kullanılan bir protokol diyebiliriz. Günümüzde web sitelerinde giriş yaparken veya kayıt olurken gördüğünüz Twitter, Facebook, Linkedin vb. kullanarak yap şeklindeki özellikler bunun örneğidir. Bu yazıda da github yetkilendirmesini kullanarak nasıl oauth yapabiliriz onu anlatacağım.
- Github uygulaması oluşturma
Öncelikle Github hesabınıza girerek "Settings/Developer settings" altında yeni bir oauth app oluşturmanız gerekiyor.
...
Burada uygulama ismini kullanıcı gördüğünde anlamlandırabileceği bir isim vermeniz doğru olur. Ana sayfa bağlantısına uygulamanızın anasayfa url değerini giriniz. callback url kısmına ise github yetkilendirmeyi tamamlandıktan sonra yönlendirmesini istediğiniz adresi yazıyorsunuz. Bu kısım yazının ilerleyen kısmında daha da iyi anlaşılacaktır. Daha sonrasında güncelleyebiliyorsunuz.
- Uygulamayı Uyarlama
Tüm oauth sağlayıcıları için ortak bir zemin oluşturabilecek aşağıdaki gibi bir arayüz oluşturdum. .Net Framework üzerinde bu işlemler için Asp.Net Identity kütüphanesi (OWIN) mevcut ama ben (paketler bağımlılık içerdiği ve kapalı kutu olduğu için) manuel yazmayı tercih ediyorum. Provider özelliği sağlayıcının ismini alacak, Authorize metodu sağlayıcının yetkilendirme sayfası bağlantısını sağlayacak, callback metodu ise sağlayıcı yetkilendirme işlemini tamamladıktan sonra yönlendirmesini yaptığında, yönlendirme sonrası ne yapılacağını tanımlayacak.
internal interface IOAuthManager
{
string Provider { get; }
string Authorize();
Task<Dictionary<string, string>> AuthorizeCallback(NameValueCollection reqParams);
}
Bu ara yüzü github için aşağıdaki gibi gerçekleştiren bir sınıf yazdım. Burada Authorize metodu bize github sağlayıcısının oauth için yönlendirmemizi istediği url adresini dönüyor. clientId ve clientSecret değerlerini oluşturduğunuz github uygulamasından almanız gerekiyor. scope kullanıcıdan almak istediğiniz detay bilgileri belirliyor. Ben sadece temel kullanıcı bilgilerini okuma ve eposta adresini okuma izni istiyorum. state değeri antiforgery ya da başka amaçlarla kullanabileceğiniz, auth sonrası github callback yaptığında gönderdiğiniz değeri aynı şekilde geri gönderdiği bir alan. Detaylı okumak için bu linki takip edebilirsiniz: Github OAuth Documentation
internal class GithubOAuthManager : IOAuthManager
{
private const string UriString = "https://github.com/";
private readonly string clientId = "";
private readonly string clientSecret = "";
private readonly string state = "login";
private readonly string scope = "read:user user:email";
private readonly string callbackRedirectUri = "";
private string accessToken;
public string Provider => "Github";
public GithubOAuthManager(string state = "login")
{
this.state = state;
var githubSettings = ConfigHelper.GetSplitValue("GithubOAuth");
clientId = githubSettings[0];
clientSecret = githubSettings[1];
}
public string Authorize()
{
var parameters = HttpUtility.ParseQueryString("");
parameters.Add("client_id", clientId);
parameters.Add("state", state);
if (!string.IsNullOrEmpty(scope))
{
parameters.Add("scope", scope);
}
var uriBuilder = new UriBuilder
{
Scheme = "https",
Host = "github.com",
Path = "login/oauth/authorize",
Query = parameters.ToString()
};
return uriBuilder.Uri.AbsoluteUri;
}
public async Task<Dictionary<string, string>> AuthorizeCallback(NameValueCollection reqParams)
{
if (!reqParams.AllKeys.Contains("code") || string.IsNullOrEmpty(reqParams["code"]))
{
throw new NotSupportedException();
}
if (!reqParams.AllKeys.Contains("state") || reqParams["state"] != state)
{
throw new NotSupportedException("ForgeryAttackAlert");
}
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(UriString);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
var request = new
{
code = reqParams["code"],
client_id = clientId,
client_secret = clientSecret,
redirect_uri = callbackRedirectUri
};
var response = await client.PostAsJsonAsync("login/oauth/access_token", request);
if (!response.IsSuccessStatusCode)
{
throw new NotSupportedException();
}
var data = await response.Content.ReadAsAsync<dynamic>();
accessToken = data.access_token;
}
var user = await GetUserData<User>("user");
var userEmails = await GetUserData<UserEmail[]>("user/emails");
if (!userEmails.Any(ue => ue.verified && ue.primary))
throw new NotSupportedException();
var email = userEmails.First(ue => ue.verified && ue.primary).email;
return new Dictionary<string, string>
{
{ "login", user.login },
{ "state", state },
{ "email", email},
{ "name", user.name},
{ "id", user.id.ToString()}
};
}
public async Task<T> GetUserData<T>(string requestPath, string token = null)
{
if (string.IsNullOrEmpty(token))
token = accessToken;
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("https://api.github.com");
client.DefaultRequestHeaders.Add("User-Agent", "Blogg");
client.DefaultRequestHeaders.Add("Authorization", $"token {token}");
var response = await client.GetAsync(requestPath);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new NotSupportedException(error);
}
var data = await response.Content.ReadAsAsync<T>();
return data;
}
}
private struct UserEmail
{
public string email;
public bool primary;
public bool verified;
public string visibility;
}
private struct User
{
public string login;
public long id;
public string avatar_url;
public string html_url;
public string name;
public string company;
public string blog;
public string twitter_username;
public int public_repos;
public int followers;
}
}
İlgili MVC controller ile kullanıcıyı Authorize sonucu dönen urle yönlendiriyorum.
public ActionResult OAuthLogin(string provider, bool register = false)
{
switch (provider)
{
case "github":
if (register)
_oAuthManager = new GithubOAuthManager("register");
else
_oAuthManager = new GithubOAuthManager();
break;
default:
//redirect or throw exception
break;
}
return Redirect(_oAuthManager.Authorize());
}
Aşağıdaki gibi bir giriş sayfası bizi karşılayacaktır.
Kullanıcı yönlendirdiğim bu sayfada yetki istediğim kapsamı da kabul edip uygulamama login olursa, github callback için uygulamamda tanımlamış olduğum sayfaya yönlendirme yapıyor. Bu yönlendirme aşamasında url üzerinden code ve state değerlerini bana iletecek. code değeri geçici bir token olup kapsama göre izin verilen kullanıcı verilerine ulaşmak üzere kullanacağım değer. Ara yüzdeki AuthorizeCallback metodu da tam olarak bu kısmı gerçekleştiriyor. code alanı ile istediğim verileri de dictionary olarak dönen bir metod. Aşağıdaki gibi kullandım.
public async Task<ActionResult> OAuthLoginCallback()
{
try
{
var dictionary = await _oAuthManager.AuthorizeCallback(HttpContext.Request.Params);
switch (_oAuthManager.Provider)
{
case "Github":
var githubLogin = dictionary["login"];
var state = dictionary["state"];
if (state == "register")
{
//add the new user to your user repo or do something else.
//redirect
}
//validate user and set your user session redirect to homepage or deny access...
//...
default:
//...
}
}
catch (Exception ex)
{
//...
}
}
Sınıf içerisinde yer alan UserEmail, User nested struct yapıları da github dokümantasyonuna göre yazdığım dönen değerleri parse etmek için kullandığım yardımcı yapılar. Diğer sağlayıcıların da mekanizması benzer şekilde. Bu yüzden diğer sağlayıcılar için de aynı arayüzü gerçekleştirmek yeterli olacaktır.