Integrazione ASP.NET e GitHub

In questo articolo voglio raccontare come si può realizzare un’applicazione integrata con sistemi esterni tramite Oauth. Questo progetto è un esempio che dimostra quanto possa essere semplice al giorno d’oggi ottenere un bel risultato di integrazione.

Il codice sorgente è ospitato presso  github  e una demo dell’applicativo è disponibile online. Vai direttamente alla app se sei curioso (PS: è su un hosting free quindi il primo accesso può essere molto lento…)

Ambito di progetto

In questo progetto volevo realizzare qualcosa di tangibile e utile per la comunità. Quindi sono partito da una necessità non ancora soddisfatta.Da sviluppatore ho incontrato un piccolo limite di GitHub. Non mostra il numero di download i un file tra i release. Differentemente da quanto avviene per altre informazioni, questa non compare tra le statistiche. Documentandomi ho scoperto che le api offrono questa possibilità. Ci sono anche dei tool client che interrogano le api per darti questa informazione, ma niente di realmente integrato. L’idea è proprio questa. Realizzare un’applicazione semplice che si integri con GitHub e mostri le statistiche, magari dando la possibilità di creare dei badge. La sfida è gestire tutto il flusso di lavoro tramite CI\CD e deplorare su un sistema di hosting per renderla pubblica, senza spendere niente.

Obiettivi

Riassumendo, gli obiettivi che mi sono posto con questa applicazione:

  1. Fare qualcosa di semplice, che funzioni e che possa essere usato da qualcuno
  2. Massimizzare il lavoro non svolto, utilizzando più possibile servizi esterni integrati tra di loro.
  3. Risolvere un problema concreto (mostrare i download su GitHub)
  4. Sperimentare (in questo caso l’integrazione Oauth2).

Ingredienti

Solution Note
Platform NET 4.5 ASP.NET MVC
Hosting AppHarbor Free ASP.NET hosting, supports CI
GitHub API Octokit Do all the hard work!
Draw badges Shields.io Great project that produces svg badges

Oauth

OAuth 2 è un protocollo autorizzativo adottato dalla maggior parte dei sistemi che devono aprire a terzi i loro servizi. In genere questo protocollo è consumato via http e trova un largo impiego in quanto con tutte le sue varianti copre una vastità infinita di casistiche. Il principio fondamentale è delegare l’autorizzazione ad un server centrale che trusta gli utenti su vari servizi (resource server). Il principio è piuttosto semplice. Voglio utilizzare la risorsa X, vado dall’authorization server, mi identifico, ottengo un token e torno dalla risorsa X che è in grado di validare il token e riconoscermi.

Immagino che oggi giorno la maggior parte degli sviluppatori abbiano già avuto a che fare con Oauth2 quindi voglio spendere il minimo indispensabile per descrivere l’implementazione in questo progetto.

Il flusso previsto da GitHub è redirect, quindi occorrono 4 semplici passaggi per ottenere il token.

 

Oauth Step 1: Autenticazione

L’utente non autenticato deve dotarsi di un token. Per questo deve navigare tramite il browser una URL esposta dal server oauth e immettere le credenziali.  Se i dati sono corretti il server oauth farà redirect al nostro applicativo, includendo in query string un codice one-shot.

    public ActionResult Login()
        {
            var client = AppConfig.Current.GetClient();
            // NOTE: this is not required, but highly recommended!
            // ask the ASP.NET Membership provider to generate a random value 
            // and store it in the current user's session
            string csrf = Membership.GeneratePassword(24, 1);
            Session["CSRF:State"] = csrf;

            var request = new OauthLoginRequest(AppConfig.Current.ClientId)
            {
                Scopes = { "user", "notifications" },
                State = csrf
            };

            // NOTE: user must be navigated to this URL
            var oauthLoginUrl = client.Oauth.GetGitHubLoginUrl(request);

            return Redirect(oauthLoginUrl.ToString());
        }

Oauth Step 2: User Authorization

Questo passaggio è costituito dall’immissione dei dati sopra descritta. Il fatto che i dati siano inseriti esclusivamente sul sito del sistema con cui ci integriamo garantisce la massima sicurezza agli utenti (le password non sono immessi in sistemi esterni).

Oauth Step 2: il parametro Code

Per incrementare il livello di siscurezza non ci viene inviato direttamente il token, ma un codice che possiamo usare per ottenere il token stesso. Questo permette di autenticarsi senza far transitare il token al di fuori del collegamento tra resource server. Il codice è in genere one-shot o a durata temporale limitata in modo da azzerare il rischio che possa essere intercettato ed usato da terzi. Il redirect può essere fatto  solo verso la “destinazione” accreditata presso l’authentication server.

Oauth Step 3: ilToken

L’ultimo passaggio (era ora) è prendere il token. In questo caso lo memorizziamo sulla sessione utente.

Questo snippet copre i punti 2 e 3.

    public ActionResult Authorize(string code, string state)
        {
                var client = AppConfig.Current.GetClient();

                if (String.IsNullOrEmpty(code))
                    return RedirectToAction("Index");

                var expectedState = Session["CSRF:State"] as string;
                if (state != expectedState) 
                throw new InvalidOperationException("SECURITY CHECK FAIL!");

                Session["CSRF:State"] = null;

                var request = new OauthTokenRequest(AppConfig.Current.ClientId
                                                   , AppConfig.Current.ClientSecret
                                                   , code);
                var token = client.Oauth.CreateAccessToken(request).Result;
                Session["OAuthToken"] = token.AccessToken;

               return new RedirectResult("~/Stats/");
        }

Integrazione tra API GitHub e ASP.NET

In questo applicativo il grosso del lavoro è svolto da octokit.net, un wrapper ben fatto delle api Git.Hub. Questo framework è ben documentato e c’è davvero tanto materiale sul sito. Tutto il flusso autorizzativo è gestito tramite le api di questo framework e per iniziare basta seguire il tutorial. Nel frammento di codice seguente è mostrato come (con il token già ottenuto) si interagisce con GitHub.

        public ActionResult Index()
        {
            var accessToken = Session["OAuthToken"] as string;
            if (accessToken != null)
            {
                var phv=new ProductHeaderValue(AppConfig.Current.AppName);
                GitHubClient client = new GitHubClient(phv);
                client.Credentials = new Credentials(accessToken);
               
                var repositories = client.Repository.GetAllForCurrent().Result;
                var user = client.User.Current().Result;                

                var totalDownload = 0;
                var totalStars = 0;
                var repoDownload = 0;
                var releasesCount = 0;
                
                //Iterate over all repos to integrate info with stats
                List<repostats> repoList = new List<repostats>();
                foreach (var currentRepo in repositories)
                {
                    repoDownload = 0;
                    releasesCount = 0;
                  
                    var relase = client.Repository.Release.GetAll(currentRepo.Id).Result;
                    if (relase.Count > 0)
                    {
                        for (int i = 0; i < relase.Count; i++)
                        {
                            for (int k = 0; k < relase[i].Assets.Count; k++)
                            {
                                var currentCount= relase[i].Assets[k].DownloadCount;
                                totalDownload += currentCount;
                                repoDownload  += currentCount;
                                releasesCount++;
                            }
                        }                      
                    }                    

                    repoList.Add(new RepoStats()
                    {
                        Repo = currentRepo,
                        TotalDownload = repoDownload,
                        ReleasesCount= releasesCount
                    });
                }
                
                 // Fill the model with data computed
                return View(new UserStats()
                {
                    Repositories = repoList,
                    User= user,
                    TotalDownload= totalDownload,
                    TotalReleases=repoModel.Sum(x=>x.ReleasesCount),
                    TotalStars=repoModel.Sum(x=>x.Repo.StargazersCount),
                    DiskUsage= repoModel.Sum(x=>x.Repo.Size)
                } );               
            }
            return new RedirectResult("~/");
        }

Shields.IO

Shields.io trasforma delle URL in badge come questo Example of badge from shields.io. La parte cool è che ogni immagine è generata tramite formato vettoriale SVG, quindi lato server tutto quello che fanno è generare del testo giusto. Temo di aver tolto molta della poesia semplificando così tanto, ma rispetto a quando si generavano dinamicamente immagini tramite rendering mi sembra un bel passo avanti. Lato nostro le cose sono ancora più semplici, ci basta generare url dinamiche per ottenere qualunque badge.

Qui di seguito uno snippet per generare il badge con il numero di download.

const string badgeTemplate = "https://img.shields.io/badge/{0}-{1}-{2}.svg";

/// <summary>
/// Exposet method that computes count and produce images using shield.io service
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ActionResult RepositoryDownloads( long id=0)
{
    if (id == 0) throw new Exception("Repository Id Missing");            
    int total=  GetDownloadCountForRepository(id);
    string url = string.Format(badgeTemplate, "downloads", total, "orange");
    return DownloadFile(url, "repositoryDownload.svg", true);
}

/// <summary>
/// Compute download count for a given repository id
/// Note: all assets of all versions are summed together
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private int GetDownloadCountForRepository(long id)
{
    int total = 0;
    GitHubClient client = GetClientForRequest(this);
    var repository = client.Repository.Get(id).Result;
    foreach (var rel in client.Repository.Release.GetAll(id).Result)
    {
        foreach (var asset in rel.Assets)
        {
            total += asset.DownloadCount;
        }
    }
    return total;
}

/// <summary>
/// Generic methods that download  and serves files
/// </summary>
/// <param name="url"> url of file to be downloaded</param>
/// <param name="filename">name of file to be served</param>
/// <param name="inline">show inline or download as attachment</param>
/// <returns></returns>
private ActionResult DownloadFile(string url, string filename, bool inline)
{
    WebClient client = new WebClient();
    var bytes = client.DownloadData(url);
    string contentType = MimeMapping.GetMimeMapping(filename);
    var cd = new System.Net.Mime.ContentDisposition
    {
        FileName = filename,
        Inline = inline,
    };

    Response.AppendHeader("Content-Disposition", cd.ToString());
    return File(bytes, contentType);
}

AppHarbor

Alla fine arrivò l’hosting. Dopo aver fatto un applicazione web in modo così agevole occorreva trovare una soluzione per ospitare il sito da qualche parte. Uno dei requisiti iniziali era integrare il codice sorgente con la continuous integration per generare gli artifatti e deployarli in maniera automatica. Dopo un attenta ricerca ho trovato AppHarbour. Questo sistema di hosting, simile ad heroku ma specializzato per .NET, è gratuito. La versione non a pagamento ha delle limitazioni (il server si spegne ogni tanto, il disco viene cancellato periodicamente, …) ma offre un sacco di funzionalità interessanti: ascolta i cambiamenti da GitHub, compila il progetto e lo deploya. Tutto gratis e semplicemente spingendo dei tasti da interfaccia.

Di questo prodotto gli aspetti interessanti:

  • Continuous deployment:  Collegandosi a GitHub si accorge ad ogni commit sul branch di produzione, compila e deploya il risultato sul server. In genere preferisco tenere la parte di deployment separata da quella di integrazione in modo da avere maggiore flessibilità e rigore nei rilasci in produzione.  In questo caso, usando un flusso di lavoro molto più snello rispetto ai canoni “aziendali”, mi va più che bene. Per chi volesse avere uno strumento free veramente completo, suggerisco invece AppVeyor (gratis per opensource).
  • Variable override: Visto che questo applicativo contiene dati sensibili come il client secret e il client id per le API (quelle che vedi nel repo sono finte, se te lo stai chiedento…)  uso la sovrascrittura delle variabili  che offre appHarbour. Le variabili del web.config vengono soprascritte in fase di build .
  • Pricing: è molto bello ricevere cose gratis, perché tutti questi strumenti ci danno l’opportunità di sperimentare e imparare cose che altrimenti richiederebbero investimenti anche economici. Senza costi e scocciature per il setup, che mi ha rubato pochissimo tempo. E il prezzo? Puoi controllare sulla pagina del sito di appHarbour per capire se può andare bene per le tue applicazioni.

“GitHub Analytics” : l’applicazione ASP.NET integrata con GitHub

Dopo questa lunga introduzione cosa rimane da dire? Dopo aver visto come funzionano le singole componenti, rimane solo da spiegare come le ho messe insieme. Semplice. Ho trovato un template HTML e creato un’interfaccia decente, vedi la gif animata.

Conclusioni

Al giorno d’oggi usiamo migliaia di applicazioni integrate tra di loro, pensiamo ai nostri telefoni o a certi dati integrati ovunque, come il login. In futuro mi aspetto che le applicazioni siano sempre più integrate tra di loro. Non parlo di quelle che rilasciano i grandi vendor, come facebook, google & co. ma delle nostre, quelle che realizziamo nel nostro piccolo per lavoro o per diletto.Oauth2 è un collante molto potente per unificare il protocollo di identificazione.

Quindi, se stai pensando a come implementare un meccanismo di autenticazione sul tuo applicativo, pensa a Oauth2 se il futuro potrebbe richiederti di far integrare altri ai tuoi sistemi.

L’altra lezione che si impara con questo progetto è che l’integrazione è il vero acceleratore per realizzare applicazioni velocemente e minimizzando il rischio di malfunzionamento. Mettere insieme cose che funzionano è sicuramente più semplice di farle daccapo. Se poi i limiti dei sistemi che integriamo opprimono la nostra applicazione, possiamo apportare cambiamenti.

 

danielefontani

Actually CTO in Sintra Consulting s.r.l, I'm senior developer and architect specialized on portals, intranets, and others business applications. Particularly interested in Agile developing and open source projects, I worked on some of this as project manager and developer. My experience include: Frameworks \Technlogies: .NET Framework (C# & VB), ASP.NET, Java, php, Spring Client languages: XML, HTML, CSS, JavaScript, Angular.js,Angular. jQuery Platforms: Sharepoint,Liferay, Drupal Databases: MSSQL, ORACLE, MYSQL, Postgres