59 messaggi dal 22 giugno 2001
Ciao a tutti,

Sono fermo da alcuni giorni su un problema di autenticazione da cui non riesco a venirne a capo.

Il client e' una pagina asp.net 4.5 che instanzia una classe proxy generata attraverso un file wsdl fornito dall'autore del web service.
Il web service, realizzato con tecnologie non Microsoft, richiede l'autenticazione basic e utilizza https.

Posto che il nome dell'istanza della classe proxy si chiama Invio. Ho provato ad inserire queste credenziali nelle seguenti proprieta':

Invio.ClientCredentials.UserName.UserName = "USER"
Invio.ClientCredentials.UserName.Password = "PASS"

invio.ChannelFactory.Credentials.UserName.UserName = "USER"
invio.ChannelFactory.Credentials.UserName.Password = "PASS"

Ho anche provato (penso sia errato) la seguente:

Dim myCredentials As New System.Net.CredentialCache
Dim netCred As New System.Net.NetworkCredential("USER", "PASS")
myCredentials.Add(New Uri(Invio.Endpoint.Address.Uri.AbsoluteUri), "Basic", netCred)
invio.ClientCredentials.Windows.ClientCredential = netCred

Il binding associato al web service e' un basicHttpsBinding, ma ho provato anche con basicHttpBinding. La sua configurazione e' la seguente:

Dim myBinding As New BasicHttpBinding()
myBinding.Security.Mode = SecurityMode.Transport
myBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic

In ogni caso il messaggio di errore che ricevo e': "Credenziali non presenti".

Ho provato anche ad usare wsHttpBinding ma, l'uso di questo binding, genera nel server del destinatario un errore 500.

Grazie dell'attenzione :-)

Sebastiano
11.886 messaggi dal 09 febbraio 2002
Contributi
Ciao Sebastiano,
non so se hai già risolto il problema, provo a risponderti lo stesso.

sem ha scritto:

Ho provato anche ad usare wsHttpBinding ma, l'uso di questo binding, genera nel server del destinatario un errore 500.

Può darsi che il servizio non supporti tutto lo stack di tecnologie WS-*. Infatti wsHttpBinding è meno interoperabile e ti conviene tornare a basicHttpBinding.

sem ha scritto:

HttpClientCredentialType.Basic
In ogni caso il messaggio di errore che ricevo e': "Credenziali non presenti".

Dev'essere che le credenziali vengono fornire a livello di messaggio, quindi come intestazioni del messaggio SOAP. Non so tu, ma io ogni volta che mi scontro con la configurazione di WCF perdo sempre un sacco di tempo.

Secondo me dovresti rinunciare a configurare il binding ed usare invece un message inspector, che ti permette di modificare la richiesta subito prima che sia inviata al servizio.
Non sarà la più elegante delle soluzioni, ma almeno in capo a 10 minuti riesci a farla funzionare.

Partiamo dalla documentazione: "Inspect or modify messages on the client".
http://msdn.microsoft.com/it-it/library/ms733786(v=vs.110).aspx
Sembra che possa fare al caso nostro. L'esempio ci chiede di scrivere alcune classi.
  • Un IClientMessageInspector. E' la classe che useremo per scrivere fisicamente l'intestazione Authorization sulla richiesta HTTP.
  • Un IEndpointBehavior. E' la classe che viene pluggata alla pipeline di WCF per modificarne il comportamento, ed è quella che si occuperà di creare un'istanza dell'IClientMessageInspector al momento opportuno.
  • Un BehaviorExtensionElement. Questo per ora non ci serve. Il suo compito è di supportare la configurazione del behavior da file .config. Lasciamolo perdere un attimo, perché possiamo configurare il behavior anche via API, mediante le proprietà della classe client Invio.


Dunque per prima cosa prepara la classe che implementa IClientMessageInspector. Può essere una cosa del genere:
public class BasicAuthenticationInspector : IClientMessageInspector
{
    private readonly string _authorization;
    public BasicAuthenticationInspector(string username, string password)
    {
        if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
            throw new ArgumentException("Devi fornire username e password");

        _authorization = "Basic " + 
        Convert.ToBase64String(
            Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, password))
            );
    }

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message messaggio, System.ServiceModel.IClientChannel channel)
    {
        //ottengo un riferimento alla richiesta http sottostante
        var richiestaHttp = messaggio.Properties.Values.OfType<HttpRequestMessageProperty>().FirstOrDefault();
        if (richiestaHttp != null)
        {
            richiestaHttp.Headers.Add("Authorization", _authorization);
        }

        return messaggio;
    }

    public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
    {
        //non ci interessa esaminare la risposta
    }
}

Gli ho dato un costruttore che richiede username e password, a ribadire che questo message inspector si occupa di scrivere delle credenziali. Nel costruttore vado subito ad assemblare il valore della header Authorization.
Invece nel metodo BeforeSendRequest ottengo un riferimento alla richiesta HTTP e aggiungo l'intestazione Authorization prevista dalla Basic Authentication.

Ora è il momento di creare l'IEndpointBehavior.
public class BasicAuthenticationBehavior : IEndpointBehavior {
    private readonly string _username;
    private readonly string _password;
    public BasicAuthenticationBehavior(string username, string password) {
        //raccolgo user e password che passerò poi al costruttore di IClientMessageInspector
        _username = username;
        _password = password;
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) {
        return;
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) {
        //aggiungo un'istanza dell'inspector
        clientRuntime.MessageInspectors.Add(new BasicAuthenticationInspector(_username, _password));
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {
        return;
    }

    public void Validate(ServiceEndpoint endpoint)
    {
        return;
    }
}

Dei 4 metodi che l'interfaccia ti chiede di implementare, solo ApplyClientBehavior è rilevante in questo caso: è quello che si occuperà di istanziare il message inspector e di passarlo al runtime.
Il costruttore si occupa solo di raccogliere user e password che verranno poi passate al message inspector.

Adesso non resta che aggiungere questo behavior alla tua classe proxy Invio. E' facilissimo.
//metti questo prima di invocare le operazioni del servizio
invio.Endpoint.EndpointBehaviors.Add(new BasicAuthenticationBehavior("Username", "Password"));

Ovviamente togli le righe in cui valorizzati le ClientCredentials. Ora username e password vengono forniti al costruttore del behavior.

Se vuoi ispezionare la richiesta che stai inviando al servizio, ti consiglio di usare Fiddler. E' un web debugger che si pone fra te e il server. Dato che il servizio è protetto da certificato SSL, allora assicurati che Fiddler sia configurato per decrittare il traffico HTTPS.

ciao,
Moreno
Modificato da BrightSoul il 05 agosto 2014 18.51 -

Enjoy learning and just keep making
59 messaggi dal 22 giugno 2001
Ciao Moreno,

Innanzitutto grazie. Sono certo che le indicazioni che mi hai fornito sono risolutive, anche perché avevo già trovato qualcosa in proposito (aggiunta dell'header Authentication (Basic + credenziali in base64) ma non avevo idea di come far funzionare questa cosa utilizzando i WS e il binding.

Purtroppo continuo a ricevere lo stesso errore, nonostante l'aggiunta delle classi che mi hai passato. Inizialmente avevo imputato il problema a qualche mio errore durante la conversione C#->VB (il mio progetto è in VB), ma poi ho provato ad usare direttamente le tue classi e ho riscontrato lo stesso problema. Dalle prove fatte con il trace, pare che non venga eseguito il seguente codice presente nella classe Inspector:

if (richiestaHttp != null)
{
  // NON VIENE ESEGUITO
  richiestaHttp.Headers.Add("Authorization", _authorization);
}


Questo è il codice che sto utilizzando:

PlaceHolder.Text = ""

        ' Per bypassare il problema del certificato non valido
        System.Net.ServicePointManager.ServerCertificateValidationCallback = Function(se As Object, cert As System.Security.Cryptography.X509Certificates.X509Certificate, chain As System.Security.Cryptography.X509Certificates.X509Chain, sslerror As System.Net.Security.SslPolicyErrors) True
        
        Dim richiesta As New demVisualizzaErogato.VisualizzaErogatoRichiesta()

        ' Il codice Ufficio contiene i tre codici di seguito nella forma CodiceRegione(3car)-CodiceAslErogatore(3car)-CodiceSsaErogatore(6car)
        ' Autenticazione Laboratorio (Sicilia)
        richiesta.codiceRegioneErogatore = "190"
        richiesta.codiceAslErogatore = "201"
        richiesta.codiceSsaErogatore = "888888"
        richiesta.pinCode = "0Gv1vsTpzlvRD9kBd8FVLo2/441rZ8rRZVf0Zi9eO6+L7kme0KC8Vx6ZjRj/4JjA4aHPTCi3D6YW9OtmYkIpW7sfchmhqHdVORBZHPitiPHdr5iIipAhIzBhFOQIPfpYasW5cQmU//uNB4GSWSNDIkaqdMrDjTPpIkbKKeoA4dQ=" ' Sicilia

        ' Ricerca della ricetta
        ' ve.cfAssistito = "PNIMRA70A01H501P" ' Utente regione Sicilia (deve essere criptato)
        richiesta.cfAssistito = "T8L5DQ4L7vZWEQ3gGWUhMYYRoGZAIsZSCZ4AieAmIKZ85B6HarO5s/jhtnp5m4r44JiwBRPYyYxtzP4npmLyk8zFSSuVxDAU3oaUdO0lOnDWKZjf886o2nbmSsPEiXYlTaJnWRU420qrQ4Sa2HfEK/32wXsOSq7TCpoMFYhkXeA="
        richiesta.nre = "1900A4000004510" ' è un campo obbligatorio

        richiesta.tipoOperazione = "1"
        ' Possibili valori (vedi pp 13/14)
        ' 1 = visualizzazione e presa in carico con recupero dati
        ' 2 = visualizzaizone e presa in carico senza recupero dati
        ' 3 = rilascio della ricetta
        ' 4 = visualizzazione dei dati dell'assistito oscurati dal medico
        ' 5 = visualizzazione e presa in carico riservata al CUP

        ' Create the binding.
        Dim myBinding As New BasicHttpsBinding()
        ' myBinding.Security.Mode = SecurityMode.Transport
        ' myBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic

        Dim ea As New EndpointAddress("https://demservicetest.sanita.finanze.it/DemRicettaErogatoServicesWeb/services/demVisualizzaErogato")

        Dim invio As New demVisualizzaErogato.visualizzaErogatoPTClient(myBinding, ea)
        ' invio.Endpoint.Address = ea
        Dim ricevuta As demVisualizzaErogato.VisualizzaErogatoRicevuta

        invio.Endpoint.EndpointBehaviors.Add(New BasicAuthenticationBehavior("UWT3CBXX", "PBUW9EBP"))

        ricevuta = invio.visualizzaErogato(richiesta)

        ' Giusto per dare un'occhiata a cosa è stato recuperato.
        If Not ricevuta Is Nothing Then
            For i As Long = 0 To ricevuta.ElencoDettagliPrescrVisualErogato.Count - 1
                PlaceHolder.Text &= "codice presidio: " & ricevuta.ElencoDettagliPrescrVisualErogato(i).codPresidio & " - "
                PlaceHolder.Text &= ricevuta.ElencoDettagliPrescrVisualErogato(i).descrProdPrestErog & "<br />"
            Next
        End If

        'Restore SSL Certificate Validation Checking
        System.Net.ServicePointManager.ServerCertificateValidationCallback = Nothing


N.B. I valori sono di test e le password sono pubbliche :)

Ho pubblicato il progetto con le classi C# a questo link:
http://www.dotcom.it/download/dem-ricetta2.zip


Ancora grazie!

Sebastiano
Modificato da sem il 07 agosto 2014 16.44 -
11.886 messaggi dal 09 febbraio 2002
Contributi
ciao,

sem ha scritto:
Dalle prove fatte con il trace, pare che non venga eseguito il seguente codice presente nella classe Inspector:

Hai ragione, anche col debugger di Visual Studio vedo che tra le proprietà non c'è alcuna HttpRequestMessageProperty, quando si usa basicHttpsBinding. Non ho idea di quale sia il motivo :) Ordine di moduli nella pipeline, immagino.

Poco male, possiamo gestire anche questo caso. Aggiungi un else a quell'if.
if (richiestaHttp != null) {
    richiestaHttp.Headers.Add("Authorization", _authorization);
} else {
    //non c'era? La creiamo noi.
    richiestaHttp = new HttpRequestMessageProperty();
    richiestaHttp.Headers.Add("Authorization", _authorization);
    messaggio.Properties.Add(HttpRequestMessageProperty.Name, richiestaHttp);
}

così dovrebbe andare. Io per lo meno vedo una risposta se modifico il progetto che mi hai mandato (a proposito: grazie, ha semplificato le cose).

ciao,
Moreno
Modificato da BrightSoul il 07 agosto 2014 18.47 -

Enjoy learning and just keep making
59 messaggi dal 22 giugno 2001
Funziona!!! 8-)

Davvero grazie infinite da parte mia e anche da parte del collega che mi aveva chiesto supporto per la creazione di un prototipo da usare come esempio per l'implementazione delle varie chiamate.

Non sono mai dovuto scendere così a basso livello per lavorare con i WS, alla SOGEI dicono di avere implementato tutto nel rispetto delle WS-I (che non conosco nel dettaglio), ma ho qualche dubbio.

Ancora grazie!

Ciao
Sebastiano
11.886 messaggi dal 09 febbraio 2002
Contributi
Bene Sebastiano, mi fa piacere che si sia risolto :)


dicono di avere implementato tutto nel rispetto delle WS-I ma ho qualche dubbio.

Digli di verificare questo: l'indirizzo del servizio dovrebbe essere pubblicamente accessibile da utenti anonimi via GET (mentre ora ho un errore 500). Dovrebbe aprirsi una pagina di cortesia che mostra le operazioni disponibili nel servizio e - molto più importante - il riferimento al WSDL che auto documenta il servizio, anch'esso accessibile in GET in forma anonima.

Tu come hai fatto a far generare le classi proxy a visual studio senza che il servizio esponesse il WSDL?

ciao,
Moreno

Enjoy learning and just keep making
59 messaggi dal 22 giugno 2001
Non si è risolto, l'hai risolto ;-)

Dicono che l'errore 500 è normale se si prova ad accedere al servizio via browser. A me un "internal server error", usato in questi casi, tanto normale non sembra. :-)

Nessuna pagina di cortesia, ho provato ad aggiungere anche la classica query string ?wsdl ma il sistema restituisce solo ed esclusivamente un errore 500. Il WSDL e gli xsd ci sono stati forniti come file di testo. Invece di puntare al percorso del servizio, ho fatto creare la classe proxy inserendo la path e il nome del file. Tant'è che, come puoi vedere nel progetto, la configurazione dell'endpoint generato by default da Visual Studio punta ad una cartella presente sul mio desktop.

Ciao
Sebastiano
11.886 messaggi dal 09 febbraio 2002
Contributi
sem ha scritto:

A me un "internal server error", usato in questi casi, tanto normale non sembra. :-)

No, infatti non è normale. Possono anche decidere di non esporre il WSDL al pubblico ma potrebbero almeno completare l'implementazione della basic authentication.
Quando il client invia una richiesta senza credenziali, il server dovrebbe:
  • Restituire uno status code http 401 (Unauthorized), per informare il client che il problema è *suo*, dato che non ha fornito dati validi
  • Invitarlo a fornire le credenziali includendo anche la header WWW-Authenticate nella risposta, come si vede qui.
    http://en.wikipedia.org/wiki/Basic_access_authentication#Server_side
    Il vantaggio è che un utente che visita il servizio dal browser potrà dunque accedere al WSDL o alla pagina di cortesia, previo inserimento dei dati di accesso, per motivi di riservatezza.


Comunque, continuando a studiare WCF ho trovato anche un altro sistema per aggiungere le credenziali alla richiesta.
Anziché arrivare al contesto per vie traverse usando un message inspector, si può arrivare direttamente per mezzo dell'oggetto OperationContextScope.

//questo blocco using abbraccia tutte le richieste alle service operations
using (new OperationContextScope(client.InnerChannel)) {
  //ottengo il riferimento alla richiesta
  var richiestaHttp = OperationContext.Current.OutgoingMessageProperties.OfType<HttpRequestMessageProperty>().FirstOrDefault();
  //qui va inserito lo stesso codice del message inspector

  //infine invoco la service operation
  var risposta = client.Operazione(parametroRichiesta);
}


Comunque continuo a preferire la soluzione basata sul message inspector, anche se comporta la creazione di qualche classe. Il motivo è che così la configurazione del servizio resta separata dal programma vero e proprio. Anzi, se ti va, implementa pure il tuo BehaviorExtensionElement, così tutto quel che riguarda l'autenticazione puoi spostarlo definitivamente nel file .config della tua applicazione.
Potresti configurare il behavior così:
<behaviors>
  <endpointBehaviors>
    <behavior name="AggiungiBasicAuthentication">
       <BasicAuthentication username="mario" password="rossi" />
    </behavior>
  </endpointBehaviors>
</behaviors>

e poi legare questa configurazione all'endpoint così:
<endpoint name="demVisualizzaErogato" behaviorConfiguration="AggiungiBasicAuthentication" address="..." binding="..." contract="..." />


ciao,
Moreno
Modificato da BrightSoul il 13 agosto 2014 10.39 -

Enjoy learning and just keep making

Torna al forum | Feed RSS

ASPItalia.com non è responsabile per il contenuto dei messaggi presenti su questo servizio, non avendo nessun controllo sui messaggi postati nei propri forum, che rappresentano l'espressione del pensiero degli autori.