Laden...

ASP.NET Core 2.2: Wie bei einem HttpClient im nachhinein die Credentials (Basic Auth) ändern?

Erstellt von GambaJo vor 4 Jahren Letzter Beitrag vor 4 Jahren 2.873 Views
GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren
ASP.NET Core 2.2: Wie bei einem HttpClient im nachhinein die Credentials (Basic Auth) ändern?

Ich arbeite an einer ASP.NET Core 2.2 Applikation, die im Prinzip eine Art Adapter für REST-Calls darstellt. Nach außen hin bietet meine Applikation eine REST-API. Diese REST-API empfängt Calls, die entweder Daten abfragen oder liefern. Diese Calls beinhalten unter anderem auch Informationen darüber, woher sich meine Applikation diese Daten her holen soll (URL, Credentials, ...). Die Applikation geht dann hin und fragt die Daten bei einer anderen API an, konvertiert diese Daten in ein bestimmtes Format, und gibt sie an den Aufrufer zurück.

Beispiel:
Kunde A schickt eine Anfrage an meine Applikation und möchte, dass meine Applikation ihm Daten aus seinem Sharepoint-Server (Sharepoint A) liefern soll. Im Call wird die URL und die Credentials des Sharepoint-Servers mit gesendet, so dass meine Applikation weiß, woher sie die Daten holen soll.
Das Gleiche kann Kunde B, C, D, ... mit seinem Sharepoint machen. Das heißt, es ist eine unbestimmte un variable Anzahl von Kunden und Sharepoint-Servern, die ich zur Entwicklungszeit nicht kenne.
Das hört sich komisch an (Kunde könnte direkt mit seinem Sharepoint kommunizieren), aber das ist nur ein kleiner Ausschnitt aus dem Anwendungsfall. Also keine Gedanken dazu machen.

Für die Kommunikation mit z.B. so einem Sharepoint-Server nutze ich den HttpClient. Nun ist der ja so konzipiert, dass man nicht pro Call eine neue Instanz erzeugt, sondern die vorhandene Instanz wieder verwendet. Leider ist es aber so, dass man dem HttpClient nur bei der Instantiierung im Konstruktor einen HttpClientHandler mit geben kann, der wiederum die Credentials beinhaltet. Ist der HttpClient ein mal instanziiert, kann ich weder neue Credentials noch einen neuen HttpClientHandler zuweisen.

Nun habe ich mir zwei Lösungen überlegt, die sich aber beide nicht gut anfühlen.

  1. Ich mache pro Call eine neue Instanz. Dann sinkt die Performance und ich habe vielleicht irgendwann so viele Socket-Verbindungen offen (die werden ja nicht sofort geschlossen), dass ich SocketExceptions bekomme.
  2. Ich erstelle mir ein statisches Dictionary in dem ich meine HttpClients verwalte (pro Host ein neuer HttpClient).

Wie gesagt, beides erscheint mir nicht empfehlenswert.
Hat jemand eine Idee, wie ich das Problem elegant auflösen kann?

656 Beiträge seit 2008
vor 4 Jahren

Wäre es eine Option, den HttpClient.SendAsync(HttpRequestMessage) Overload zu benutzen? Da kannst du pro Request die Optionen setzen (und somit in Folge den gleichen Client benutzen).

16.835 Beiträge seit 2008
vor 4 Jahren

Leider ist es aber so, dass man dem HttpClient nur bei der Instantiierung im Konstruktor einen HttpClientHandler mit geben kann, der wiederum die Credentials beinhaltet.

Warum hat der Client Handler die Credentials?

Du kannst den Token doch direkt dem HttpClient zuweisen.

 httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

Hab exakt die gleiche Konstellation von HttpClient zu SharePoint, wobei ich noch auf die empfohlene Variante via HttpClientFactory setze.
Aber via HttpRequestMessage die Header zu setzen ist auch eine gute Variante - Deine aktuelle jedoch nicht 😃

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Danke für die schnellen Antworten.

Wäre es eine Option, den
>
Overload zu benutzen? Da kannst du pro Request die Optionen setzen (und somit in Folge den gleichen Client benutzen).

Das hört sich erst mal gut an. Werde ich gleich mal versuchen.

Warum hat der Client Handler die Credentials?

Ich habe mich an Beispiele, wie dieses gehalten. Dort wird der HttpClientHandler mit den Credentials gefüttert. Wie man im Nachhinein diesen oder die Credentials ändern kann, habe ich nicht herausfinden können.

Du kannst den Token doch direkt dem HttpClient zuweisen.

Habe derzeit nur Benutzer und Passwort.

Hab exakt die gleiche Konstellation von HttpClient zu SharePoint, wobei ich noch auf die empfohlene Variante via HttpClientFactory setze.

Würde ich mir auch gerne anschauen. Hast Du da einen Link mit guten und aussagekräftigen Beispielen?

16.835 Beiträge seit 2008
vor 4 Jahren

Ich habe mich an Beispiele, wie
>
gehalten. Dort wird der HttpClientHandler mit den Credentials gefüttert. Wie man im Nachhinein diesen oder die Credentials ändern kann, habe ich nicht herausfinden können.

Das ist prinzipiell kein gutes Beispiel für ein Multi-Tenant-System.
Das zeigt nur eine einfachen Call - nichts mit DI oder Tenant-Trennung.

Habe derzeit nur Benutzer und Passwort.

Die am wenigsten empfohlene Variante von allen - und in den neuen SharePoint APIs aus Sicherheitsgründen auch nicht mehr möglich 😉
Die neuen APIs erlauben nur noch MSAL via OAuth2 Client Certificate Flow.

Hast Du da einen Link mit guten und aussagekräftigen Beispielen?

Nichts anderes als eine Registrierung der Factory.

Und dann optimalerweise eben die HttpRequestMessage nutzen, wenn Multi Tenant.

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Das ist prinzipiell kein gutes Beispiel für ein Multi-Tenant-System.

Das ist richtig. Das habe ich nur in meinem Test-Projekt verwendet, um zu evaluieren, wie ich grundsätzlich mit dem Sharepoint kommunizieren kann. Wollte das dann in mein produktives Projekt (ASP.NET Core 2.2) übertragen.

Tatsächlich nutze ich in meinem produktiven Projekt bereits die HttpClientFactory per DI. Das ist ja im Grunde das Problem. Diese bietet nur die Methode CreateClient(), die mir einen fertigen HttpClient liefert. Da kann ich weder Credentials, noch einen neuen HttpClientHandler zuweisen.

Ich habe es jetzt auch mal mal mit HttpRequestMessage versucht, aber auch da kann ich keine Credentials setzen. Ich kann zwar einen Authorization-Header erstellen, bekomme dann aber trotzdem ein 403 zurück.

16.835 Beiträge seit 2008
vor 4 Jahren

Zeig mal, was Du gemacht hast; wenn Du die Methode HttpClient.SendAsync(HttpRequestMessage) richtig implementierst, kann das nicht anders sein.

Credentials haben im Handler ohnehin nichts zu suchen; da sind sie so oder so falsch.

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Aus meinem Test-Projekt (.NET Core 2.2)

        private string GetAsync(string realtiveUrl, SharePointOnlineCredentials credentials)
        {
            Console.WriteLine($"GET { realtiveUrl }");
            using (var handler = new HttpClientHandler() { Credentials = credentials })
            {
                Uri uri = new Uri(_baseAddress);
                handler.CookieContainer.SetCookies(uri, credentials.GetAuthenticationCookie(uri));

                using (HttpClient httpClient = new HttpClient(handler)
                {
                    BaseAddress = new Uri(_baseAddress)
                })
                {
                    SetRequestHeaders(httpClient);

                    using (HttpResponseMessage response = httpClient.GetAsync(realtiveUrl).Result)
                    {
                        Console.WriteLine($"GetAsync() StatusCode: {(int)response.StatusCode} {response.StatusCode}");
                        if (response.IsSuccessStatusCode)
                        {
                            return response.Content.ReadAsStringAsync().Result;
                        }
                        else
                        {
                            Console.WriteLine(response.Content.ReadAsStringAsync().Result);
                            return string.Empty;
                        }
                    }
                }
            }
        }

Ergebnis: 200 Ok

Aus meinem Produktiv-Projekt (ASP.NET Core 2.2)

        protected async Task<ContentString> GetAsync(TargetSharepointApi targetSharepointApi, string relativeUri)
        {
            Uri requestUri = CreateUri(targetSharepointApi.BaseUrl, relativeUri);
            using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri))
            {
                
                httpRequestMessage.Headers.Add("accept", "application/json;odata=verbose");
                httpRequestMessage.Headers.Add("accept-language", "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7");
                httpRequestMessage.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8"));
                httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes("<user>:<password>")));
                using (HttpResponseMessage response = await _httpClient.SendAsync(httpRequestMessage))
                {
                    if (!response.IsSuccessStatusCode)
                    {
                        throw GetECMException(response);
                    }

                    using (HttpContent content = response.Content)
                    {
                        return new ContentString()
                        {
                            Content = await content.ReadAsStringAsync(),
                            ContentType = content.Headers.ContentType.ToString()
                        };
                    }
                }
            }            
    }

User und Passwort wurde natürlich ersetzt.
Ergebnis: 403 Forbidden

{
	"error": {
		"code": "-2147024891, System.UnauthorizedAccessException",
		"message": {
			"lang": "en-US",
			"value": "Access denied. You do not have permission to perform this action or access this resource."
		}
	}
}
16.835 Beiträge seit 2008
vor 4 Jahren

Naja, das heisst zumindest, dass Du authentifiziert bist - aber eben nicht authorisiert.
Wenn Auth generell nicht funktionieren würde, würde eine andere Fehlermeldung mit Status Code 301 kommen.

Ich kenne aber gar keine API mehr die überhaupt Username und Password in der Form akzeptiert.
Daher kann ich Dir an der Stelle auch nicht mehr weiter helfen. Das prinzipielle Auth funktioniert ja auch bei Dir.

Vergleich halt mal die Requests via Fiddler.

PS: auch in Konsolenanwendungen kannst Du problemlos Dependency Injection nutzen.

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Daher kann ich Dir an der Stelle auch nicht mehr weiter helfen.

Trotzdem danke für deine Mühe. Wie machst Du das denn? Vielleicht würde mir das helfen?

Daher kann ich Dir an der Stelle auch nicht mehr weiter helfen.
Vergleich halt mal die Requests via Fiddler.

Werde ich machen.

PS: auch in Konsolenanwendungen kannst Du problemlos Dependency Injection nutzen.

Wollte eine möglichst einfache Test-Anwendung, um Seiteneffekte auszuschließen. Wäre auch nicht auf die Idee gekommen, dass DI Probleme macht. Wobei das nicht die DI ist, sondern die Kombination aus DI, dem Wiederverwenden des HttpClient und der Anforderung von wechselnden Credentials.
Andererseits kann ich mir irgendwie nicht vorstellen, dass das Framework da so starr ist.

16.835 Beiträge seit 2008
vor 4 Jahren

Wir sprechen mit SharePoint über Azure Active Directory; also OAuth via MSAL - hier als Beispiel als technischen User.
MSAL bedeutet, dass die App in Azure AD registriert sein muss, eine Client ID und ein Zertifikat x509 hinterlegt sein muss.
Alle anderen Varianten werden nicht mehr unterstützt und resultieren in einem ungültigen Token (Invalid Token Message).

Den Token bekommt man über

            
            byte[] certBytes = Convert.FromBase64String(options.CertificateFileContents);
            _certificate = new X509Certificate2(certBytes, options.CertificatePass);

            // https://blog.mastykarz.nl/azure-ad-app-only-access-token-using-certificate-dotnet-core/
            string authority = $"{options.Authority}{options.TenantId}";

            AppContext = ConfidentialClientApplicationBuilder.Create(options.ClientId)
                .WithAuthority(authority)
                .WithCertificate(_certificate)
                .WithRedirectUri(options.RedirectUri)
                .Build();

Anschließend kann man sich den Token generieren.


        protected async Task<string> GetAuthenticationTokenAsync(string[] scopes, CancellationToken cancellationToken = default)
        {
            AuthenticationResult authResult = await AppContext.AcquireTokenForClient(scopes).ExecuteAsync(cancellationToken);

            return authResult.AccessToken;
        }

scopes ist bei MSAL gegen SharePoint hier generell https://<tenantName>.sharepoint.com/.default.
Die tatsächlichen Scopes werden nicht mehr über Code sondern über das Adminportal in Azure konfiguriert.

Der Token ist nun das Stück, das man für die Authentifzierung verwendet:

Authorization = new AuthenticationHeaderValue("Bearer", token);

Alles weitere ist dann ein ganz normaler Request.

Man kann das auch alles via On Behalf Flow machen; das heisst "im Namen des Users" - benötigt dafür kein Zertifikat.
On Behalf Flow nimmt den aktuellen Token des Users, fragt bei Azure AD nach einem neuen Token im Context des Users für den Zugriff auf SharePoint - fertig.


        public Task<AuthenticationResult> AuthenticateAsync(string accessToken, string userId)
        {
            return AuthenticateAsync(new UserAssertion(accessToken, AssertionType, userId));
        }

        public async Task<AuthenticationResult> AuthenticateAsync(UserAssertion userAssertion)
        {
            AuthenticationContext authContext = new AuthenticationContext(_authority);
            ClientCredential clientCredential = new ClientCredential(_clientId, _clientSecret);

            AuthenticationResult result = await authContext.AcquireTokenAsync(_resource, clientCredential, userAssertion);

            return result;
        }

bzw. geht das mit der neuen Api auch mit AcquireTokenForUser; hab ich aber noch nicht drauf umgebaut.

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Wir sprechen mit SharePoint über Azure Active Directory

Interessant. Funktioniert das auch mit Sharepoint on premise?

16.835 Beiträge seit 2008
vor 4 Jahren

Ich hatte Dich so verstanden, dass Du "SharePoint Online" verwendest und damit nicht On-Prem bist. Sorry.
Ein On-Prem SharePoint kann mit einem Azure Active Directory angebunden sein - muss aber nicht.

Verwendet ihr ein lokales AD, dann funktionieren meine Tipps natürlich nicht.
Ein lokales AD hat aber auch keine Token-Funktionalität (außer ihr verwendet ADFS) und damit funktionier der On Behalf Flow auch nicht.

Wüsste nicht mal (mehr), wie da eine Impersonation funktioniert.

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Ich hatte Dich so verstanden, dass Du "SharePoint Online" verwendest und damit nicht On-Prem bist. Sorry.

Die Applikation, an der ich arbeite sollte am besten beides können (Online und On-Prem). Es gibt halt solche und solche Unternehmen in DE.

Na gut, fühlt sich irgendwie falsch an, aber ich habe keine bessere Lösung, als einen HttpClient pro Host zu instantiieren und diese dann in einem Dictionary vor zu halten.

16.835 Beiträge seit 2008
vor 4 Jahren

Die Applikation, an der ich arbeite sollte am besten beides können (Online und On-Prem).

Musst halt mehrere Identity Provider (so nennt man sie jedenfalls meistens) entwickeln und konfigurieren.
Ist ja mit / dank DI super easy.

Na gut, fühlt sich irgendwie falsch an, aber ich habe keine bessere Lösung, als einen HttpClient pro Host zu instantiieren und diese dann in einem Dictionary vor zu halten.

Dir wurde ja jetzt auch mehrmals gezeigt, dass man einen HttpClient problemlos injizieren kann und die Credentials (auch Basic!) auch ohne Handler setzen kann.
Gibt also keine Notwendigkeit, das pro Host zu tun. Skaliert ja auch überhaupt nicht 😉

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Ist ja mit / dank DI super easy.

Joa, ich bin da noch nicht so fit drin X(

Dir wurde ja jetzt auch mehrmals gezeigt, dass man einen HttpClient problemlos injizieren kann und die Credentials (auch Basic!) auch ohne Handler setzen kann.

Jetzt stehe ich auf dem Schlauch. Wo wurde mir das gezeigt?

Ich habe den Traffic per Fiddler untersucht. Es sind nicht die Credentials, zumindest nicht direkt, sondern der daraus resultierende Cookie:

GET /_api/web/lists HTTP/1.1
Accept: application/json; odata=verbose
Accept-Language: de-DE, de; q=0.9, en-US; q=0.8, en; q=0.7
Accept-Charset: utf-8
Cookie: SPOIDCRL=77u/PD94bWwgdmxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Host: xxx.sharepoint.com

Ich habe aus meinem oberen Beispiel auch aus dieser Zeile:

using (var handler = new HttpClientHandler() { Credentials = credentials })

das "Credentials = credentials" entfernt, und es hat trotzdem funktioniert, weil der Cookie korrekt gesetzt wurde.

Das heißt, ich bräuchte nur eine HttpClient-Instanz, wenn ich den Cookie im Nachhinein immer wieder neu setzen könnte. In meinem oberen Beispiel wird der Cookie über den HttpClientHandler gesetzt:

handler.CookieContainer.SetCookies(uri, credentials.GetAuthenticationCookie(uri));

An den komme ich ja nicht mehr dran, wenn der HttpCLient damit ein mal instantiiert wurde. Gibt es dafür noch eine andere elegante Methode?

16.835 Beiträge seit 2008
vor 4 Jahren

Wo wurde mir das gezeigt?

Du sagst ja, dass Du die Basic Credentials via Handler setzt; die kannst Du problemlos in der RequestMessage mit schicken und brauchst so den Handler überhaupt nicht.

Host: xxx.sharepoint.com

Ist das nur ein Sample Host? Weil sharepoint.com suggeriert, dass wir doch von SharePoint Online sprechen und nicht von On Prem.

das "Credentials = credentials" entfernt, und es hat trotzdem funktioniert, weil der Cookie korrekt gesetzt wurde.

Naja; in dem einen Fall hast Du wohl eine Authentifizierung via Cookie, im anderen via Basic Auth.
Das sind zwei verschiedene paar Stiefel. Mir fehlt hier aber das Wissen, wieso Du hier überhaupt mit einem Cookie arbeitest.

Wird Basic Auth für das Erstellen des Cookies benötigt, und dann arbeitest Du bei allen weiteren Requests mit dem Cookie oder wie?

So wie ich die Dokumentation dazu sehe, kannst Du das problemlos alles jedes mal neu erzeugen - musst nichts zwischen speichern.
Aber Du musst für beide Welten (On Prem und Cloud) zwei völlig verschiedene Ansätze fahren, wenn das nur über den Cookie geht.

Evtl so (grob - kann man noch ausschmücken).


namespace  A
{
    public interface ISharePointIdentityProvider
    {
        HttpRequestMessage GetRequestMessage(string uri);
    }

    public class SharePointOnPremProvider : ISharePointIdentityProvider {
        SharePointOnPremProviderOptions _options;
        public SharePointOnPremProvider(IOptions<SharePointOnPremProviderOptions> option)
        {
            _options = options.Value;
            _httpClientFactory = httpClientFactory;
        }

        public CookieContainer  GetCookieContainer(string uri)
        {
            string user = _options.User;
            string password = _options.Password;

            SecureString securePassword = new SecureString();
            foreach (var c in password)
            { 
                securePassword.AppendChar(c);
            }

            SharePointOnlineCredentials credentials = new SharePointOnlineCredentials(user, securePassword);
            var authCookie = credentials.GetAuthenticationCookie(uri);
            CookieContainer cookieContainer = new CookieContainer();
            {
                cookieContainer.SetCookies(uri, authCookie);
            }

            return cookieContainer;
        }

        public HttpRequestMessage GetRequestMessage(string uri)
        {
            var message = new HttpRequestMessage(HttpMethod.Get, uri);
            var cookieContainer = GetCookieContainer(uri);
            // über die cookies iterieren und HttpRequestMessage.Headers.Add() zuweisen
            

            return message;
        }
    }

    public class SharePointOnlineProvider : ISharePointIdentityProvider {
        SharePointOnlineProviderOptions _options;
        public SharePointOnlineProvider(IOptions<SharePointOnlineProviderOptions> option, IHttpClientFactory httpClientFactory)
        {
            _options = options.Value;
            _httpClientFactory = httpClientFactory;
        }

        public string GetToken()
        {
            // Token logik
            return token;
        }

        public HttpRequestMessage GetRequestMessage(string uri)
        {
            var message = new HttpRequestMessage(HttpMethod.Get, uri);

            // hier den token im header hinzufügen

            return message;
        }
    }
}

Es kann aber sein, dass Du pro Verfahren eine Client-Instanz brauchst - aber nicht für jeden Host.

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Fühl dich geknutscht 😁
Das hat mir vorerst weiter geholfen.

Ist das nur ein Sample Host? Weil sharepoint.com suggeriert, dass wir doch von SharePoint Online sprechen und nicht von On Prem.

Zum Entwickeln habe ich mal eine Azure-Instanz, weil der erste Kunde das so braucht. Später soll aber auch on-prem möglich sein.

Naja; in dem einen Fall hast Du wohl eine Authentifizierung via Cookie, im anderen via Basic Auth.
Das sind zwei verschiedene paar Stiefel. Mir fehlt hier aber das Wissen, wieso Du hier überhaupt mit einem Cookie arbeitest.

BasicAuth war hier in meinem Code nur ein verzweifelter Versuch. Also einer von vielen verzweifelten Versuchen.
Den Cookie nutze ich, weil ich vom Kunden nur User und Passwort bekommen habe und das Netz in diesem Fall das Verfahren mit dem Erzeugen eines Cookies ausgespuckt hat.

Wird Basic Auth für das Erstellen des Cookies benötigt, und dann arbeitest Du bei allen weiteren Requests mit dem Cookie oder wie?

Vermutlich nicht. War aber so in den Beispielen drin und es hat auf Anhieb funktioniert. Im Nachhinein macht das natürlich nicht so viel Sinn.

Es kann aber sein, dass Du pro Verfahren eine Client-Instanz brauchst - aber nicht für jeden Host.

Damit kann ich gut leben. Ich werde zunächst die Online-Variante mit allem, was dazu gehört, implementieren. Wenn dann die Anforderung für On-Prem kommt, werde ich nur die Authentifizierung um den entsprechenden Part erweitern.

16.835 Beiträge seit 2008
vor 4 Jahren

Hab mal mit meinem SharePoint Profi Kollegen geredet.

Bei SharePoint OnPrem gibt es nicht nur einen Weg: das kommt drauf an wie der SharePoint und der IIS drunter konfiguriert ist.
Je nachdem, welche Auth Methode dort konfiguriert ist, musst Du programmieren.

Auf den Cookie versucht man wohl generell zu verzichten; sondern:
Windows Auth > Basic Auth > Cookie.

Manche SharePoint OnPrem gehen auch über den ADFS - und da brauchst den Bearer Token., wie Azure Active Directory.

GambaJo Themenstarter:in
105 Beiträge seit 2006
vor 4 Jahren

Danke für die Hilfe. Vom Gefühl her würde ich bei dem Projekt OnPrem eher auf BasicAuth setzen. Das wäre dann aber einen neuen Thread wert.
Ich glaub's zwar nicht, aber vielleicht habe ich das Glück, dass bis dahin sich OnPrem von alleine erledigt hat, weil kein Bedarf.