Keycloak adaptor for golang application - authentication
I am going to secure my golang application using keycloak, but keycloak itself does not support go language.
There are some go adaptor as an open project in github that has implemented openId connect protocol as a provider service, but they do not provide an example or documentation on how to integrate libraries with an application.
How can i interact with keycloak using golang?
As you have pointed out, there is no official keycloak adapter for golang.
But it is pretty straight forward to implement it. Here is a little walk through.
Keycloak server
For this example, I will use the official keycloak docker image to start the server.
The version used is 4.1.0.Final. I think this will work with older KeyCloak versions too though.
docker run -d -p 8080:8080 -e KEYCLOAK_USER=keycloak -e KEYCLOAK_PASSWORD=k --name keycloak jboss/keycloak:4.1.0.Final
After the server is up and running, you can open localhost:8080/auth in your browser, navigate to the administration console and login with username keycloak and k as the corresponding password.
I will not go through the complete process of creating a realm/clients/users. You can look this up under
https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console
Here is just an outline for what I did to reproduce this example:
create a realm named demo
turn off the requirement of ssl for this realm (realmsettings -> login -> require ssl)
create a client named demo-client (change the "Access Type" to confidential)
create a user named demo with password demo (users -> add user). Make sure to activate and impersonate this user.
configure the demo-client to be confidential and use http://localhost:8181/demo/callback as a valid redirect URI.
The resulting keycloak.json (obtained from the installation tab) looks like this:
{
"realm": "demo",
"auth-server-url": "http://localhost:8080/auth",
"ssl-required": "none",
"resource": "demo-client",
"credentials": {
"secret": "cbfd6e04-a51c-4982-a25b-7aaba4f30c81"
},
"confidential-port": 0
}
Beware that your secret will be different though.
The Go Server
Let's go over to the go server. I use the github.com/coreos/go-oidc package for the heavy lifting:
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"strings"
oidc "github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
func main() {
configURL := "http://localhost:8080/auth/realms/demo"
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, configURL)
if err != nil {
panic(err)
}
clientID := "demo-client"
clientSecret := "cbfd6e04-a51c-4982-a25b-7aaba4f30c81"
redirectURL := "http://localhost:8181/demo/callback"
// Configure an OpenID Connect aware OAuth2 client.
oauth2Config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
state := "somestate"
oidcConfig := &oidc.Config{
ClientID: clientID,
}
verifier := provider.Verifier(oidcConfig)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
rawAccessToken := r.Header.Get("Authorization")
if rawAccessToken == "" {
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
return
}
parts := strings.Split(rawAccessToken, " ")
if len(parts) != 2 {
w.WriteHeader(400)
return
}
_, err := verifier.Verify(ctx, parts[1])
if err != nil {
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
return
}
w.Write([]byte("hello world"))
})
http.HandleFunc("/demo/callback", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("state") != state {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
return
}
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
resp := struct {
OAuth2Token *oauth2.Token
IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
}{oauth2Token, new(json.RawMessage)}
if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data, err := json.MarshalIndent(resp, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
})
log.Fatal(http.ListenAndServe("localhost:8181", nil))
}
This program starts a regular http server with two endpoints. The first one ("/") is your regular endpoint that handles
application logic. In this case, it only returns "hello world" to your client.
The second endpoint ("/demo/callback") is used as a callback for keycloak. This endpoint needs to be registered on your
keycloak server. Keycloak will issue a redirect to this callback URL upon successful user authentication. The redirect contains some additional query parameters. These parameters contain a code that can be used to obtain access/id tokens.
Verify your setup
In order to test this setup you can open a webbrowser and navitage to http://localhost:8181.
The request should reach your go server, which tries to authenticate you. Since you did not send a token, the go server
will redirecty you to keycloak to authenticate.
You should see the login screen of keycloak. Login with the demo user you have created for this realm (demo/demo).
If you have configured your keycloak correctly, it will authenticate you and redirect you to your go server callback.
The end result should be a json like this
{
"OAuth2Token": {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsc1hHR2VxSmx3UUZweWVYR0x6b2plZXBYSEhXUngtTHVJTVVLdDBmNmlnIn0.eyJqdGkiOiI5ZjAxNjM2OC1lYmEwLTRiZjMtYTU5Ni1kOGU1MzdmNTNlZGYiLCJleHAiOjE1MzIxNzM2NTIsIm5iZiI6MCwiaWF0IjoxNTMyMTczMzUyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6ImRlbW8tY2xpZW50Iiwic3ViIjoiMzgzMzhjOGItYWQ3Zi00NjlmLTgzOTgtMTc5ODk1ODFiYTEyIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZGVtby1jbGllbnQiLCJhdXRoX3RpbWUiOjE1MzIxNzMzNTIsInNlc3Npb25fc3RhdGUiOiJjZTg2NWFkZC02N2I4LTQ5MDUtOGYwMy05YzE2MDNjMWJhMGQiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6ImRlbW8iLCJlbWFpbCI6ImRlbW9AZGVtby5jb20ifQ.KERz8rBddxM9Qho3kgigX-fClWqbKY-3JcWT3JOQDoLa-prkorfa40BWlyf9ULVgjzT2d8FLJpqQIQYvucKU7Q7vFBVIjTGucUZaE7b6JGMea5H34A1i-MNm7L2CzDJ2GnBONhNwLKoftTSl0prbzwkzcVrps-JAZ6L2gssSa5hBBGJYBKAUfm1OIb57Jq0vzro3vLghZ4Ay7iNunwfcHUrxiFJfUjaU6PQwzrA5pnItOPuavJFUgso7-3JLtn3X9GQuyyZKrkDo6-gzU0JZmkQQzAXXgt43NxooryImuacwSB5xbIKY6qFkedldoOPehld1-oLv0Yy_FIwEad3uLw",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsc1hHR2VxSmx3UUZweWVYR0x6b2plZXBYSEhXUngtTHVJTVVLdDBmNmlnIn0.eyJqdGkiOiI0MjdmMTlhYy1jMTkzLTQ2YmQtYWFhNi0wY2Q1OTI5NmEwMGQiLCJleHAiOjE1MzIxNzUxNTIsIm5iZiI6MCwiaWF0IjoxNTMyMTczMzUyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6ImRlbW8tY2xpZW50Iiwic3ViIjoiMzgzMzhjOGItYWQ3Zi00NjlmLTgzOTgtMTc5ODk1ODFiYTEyIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImRlbW8tY2xpZW50IiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiY2U4NjVhZGQtNjdiOC00OTA1LThmMDMtOWMxNjAzYzFiYTBkIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIn0.FvvDW6ZSH8mlRR2zgaN1zesX14SmkCs9RrIVU4Jn1-SHVdKEA6YKur0-RUAFTObQDMLVhFFJ05AjGVGWpBrgVDcAwW2pI9saM-OHlyTJ3VfFoylgfzakVOIpbIDnHO12UaJrkOI9NWPAJdbBOzBHfsDhKbxhjg4ZX8SwlKr42rV4WWuSRcNu4_YDVO19SiXSCKXVldZ1_2S-qPvViq7VZfaoRLHuYyDvma_ByMsmib9JUkevJ8dxsYxVQ5FWaAfFanh1a1f8HxNRI-Cl180oPn1_Tqq_SYwxzBCw7Q_ENkMirwRS1a4cX9yMVEDW2uvKz2D-OiNAUK8d_ONuPEkTGQ",
"expiry": "2018-07-21T13:47:28.986686385+02:00"
},
"IDTokenClaims": {
"jti": "f4d56526-37d9-4d32-b99d-81090e92d3a7",
"exp": 1532173652,
"nbf": 0,
"iat": 1532173352,
"iss": "http://localhost:8080/auth/realms/demo",
"aud": "demo-client",
"sub": "38338c8b-ad7f-469f-8398-17989581ba12",
"typ": "ID",
"azp": "demo-client",
"auth_time": 1532173352,
"session_state": "ce865add-67b8-4905-8f03-9c1603c1ba0d",
"acr": "1",
"email_verified": true,
"preferred_username": "demo",
"email": "demo#demo.com"
}
}
You can copy your access token and use curl to verify if the server is able to accept your tokens:
# use your complete access token here
export TOKEN="eyJhbG..."
curl -H "Authorization: Bearer $TOKEN" localhost:8181
# output hello world
You can try it again after the token has expired - or temper with the token. In case you do it, you should get a redirect to
your keycloak server again.
There is also the gocloak library which provides lot's of functionality. The lib is in active development and allready in use in real world projects. So possible bugs & feature requests are being handled.
It provides administration features like "CreateUser","CreateGroup" etc. and also provides functions for Login, Token validation, etc.
For example creating a user is as easy as:
client := gocloak.NewClient("https://mycool.keycloak.instance")
token, err := client.LoginAdmin("user", "password", "realmName")
if err != nil {
panic("Something wrong with the credentials or url")
}
user := gocloak.User{
FirstName: "Bob",
LastName: "Uncle",
EMail: "something#really.wrong",
Enabled: true,
Username: "CoolGuy",
}
client.CreateUser(token.AccessToken, "realm", user)
if err != nil {
panic("Oh no!, failed to create user :(")
}
It does also supports Introspecting a Requesting Party Token
client := gocloak.NewClient(hostname)
token, err := client.LoginClient(clientid, clientSecret, realm)
if err != nil {
panic("Login failed:"+ err.Error())
}
rptResult, err := client.RetrospectToken(token.AccessToken, clientid, clientSecret, realm)
if err != nil {
panic("Inspection failed:"+ err.Error())
}
if !rptResult.Active {
panic("Token is not active")
}
permissions := rptResult.Permissions
//Do something with the permissions ;)
Also to handle easy authentication & token refresh when using echo there is another lib based on gocloak called gocloak-echo. This lib provides handler & middleware to help out, but is still in a more WIP state.
The library also provides decoding of accessTokens into custom claims
Disclosure: I am the (main) author of gocloak, so it's also a little advertising, but in general it answers the question. I had the same problem as the author and i decided to create my own lib (based on the lib of someone else, as stated in the readme on github).
Related
Can you customize Ktor 401 - Unauthorized Response?
When implementing Basic Authentication on Ktor and configuring a Provider, which validates whether the credentials are legit by returning a non null Principal, like in this example: install(Authentication) { basic("auth-basic") { realm = "Access to the '/' path" validate { credentials -> if (credentials.name == "fernando" && credentials.password == "foobar") { UserIdPrincipal(credentials.name) } else { null } } } } If the credentials are invalid and a null is returned, then Ktor automatically communicates with the client by triggering a 401 - Unauthorized, which in terms of behavior is what is expected... But I cannot provide/add any extra information, like for example where exactly the issue was: username or password. Any idea on how to mitigate this?
for resolve this problem you can use StatusPages by install it on application calss. like below: install(StatusPages) { status(HttpStatusCode.Unauthorized) { call.respond(HttpStatusCode.Unauthorized, "Your Response Object") } } for more informatin please read these links: https://ktor.io/docs/status-pages.html https://github.com/ktorio/ktor/issues/366
The message when the token expires can be shown using StatusPages or by using the challenge method in the JWTAuth class like this: jwt { challenge { _, _ -> call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") } }
Web api set name and password to request.BasicAuth
The route for login authenticate -- app.Handle("GET", "/v1/users/token", u.Token). We can get name and password from request.BasicAuth. func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { ... name, pass, ok := r.BasicAuth() ... } But how can I set name and pass from client web url?
Before a browser asks the user to supply basic auth credentials, you have to deny the request access (using status code 401 Unauthorized). You should set the header WWW-Authenticate to Basic realm="Your message". Also see this article. So in your code, when ok is false, you should deny that request: func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { ... name, pass, ok := r.BasicAuth() if !ok { w.Header().Set("WWW-Authenticate", "Basic realm=\"Your message\"") http.Error(w, "Must supply authentication credentials", http.StatusUnauthorized) return } }
googleapi: Error 400: Bad Request, failedPrecondition
I'm trying to get message information from the Google API using a service account and a JWT. I've already enabled Domain wide delegation. I should probably mention that I'm not a GSuite admin, just a user who is trying to get messages from my own Inbox using a Go script. func main() { data, err := ioutil.ReadFile("JWT_Credentials.json") conf, err := google.JWTConfigFromJSON(data, gmail.GmailReadonlyScope) client := conf.Client(oauth2.NoContext) // Create a new gmail service using the client gmailService, err := gmail.New(client) fullMessage, err := gmailService.Users.Messages.Get("me", message.Id).Format("metadata").Do() if err != nil { log.Printf("Error: %v", err) } } and my JWT credentials are as follows: { "type": "service_account", "project_id": "PROJECT_ID", "private_key_id": "PRIVATE_KEY_ID", "private_key": "-----BEGIN PRIVATE KEY-----\PRIVATE_KEY", "client_email": "SERVICE#PROJECT_ID.iam.gserviceaccount.com", "client_id": "CLIENT_ID", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "CERT_URL" } I get the following error when the code runs: googleapi: Error 400: Bad Request, failedPrecondition If anybody has the solution to this problem or has encountered it in the past, please help me out!
using package Azure/go-ntlmssp to authenticate sharepoint results 401
i have used package like github.com/Azure/go-ntlmssp to authenticate to sharepoint 2010. url, username, password := "http://www.some-website.com", "admin", "12345" client := &http.Client{ Transport: ntlmssp.Negotiator{ RoundTripper:&http.Transport{}, }, } req, _ := http.NewRequest("GET", url, nil) req.SetBasicAuth(username, password) res, _ := client.Do(req) in response i am getting 401 Unauthorized Now here the wierd thing is sometimes(after many trials and not consistent after that also) the authentication happens successfully. And when the authentication fails in response header i can see Www-Authenticate:[NTLM]. Please suggest
Mocking HTTPS responses in Go
I'm trying to write tests for a package that makes requests to a web service. I'm running into issues probably due to my lack of understanding of TLS. Currently my test looks something like this: func TestSimple() { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) fmt.Fprintf(w, `{ "fake" : "json data here" }`) })) transport := &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(server.URL) }, } // Client is the type in my package that makes requests client := Client{ c: http.Client{Transport: transport}, } client.DoRequest() // ... } My package has a package variable (I'd like for it to be a constant..) for the base address of the web service to query. It is an https URL. The test server I created above is plain HTTP, no TLS. By default, my test fails with the error "tls: first record does not look like a TLS handshake." To get this to work, my tests change the package variable to a plain http URL instead of https before making the query. Is there any way around this? Can I make the package variable a constant (https), and either set up a http.Transport that "downgrades" to unencrypted HTTP, or use httptest.NewTLSServer() instead? (When I try to use NewTLSServer() I get "http: TLS handshake error from 127.0.0.1:45678: tls: oversized record received with length 20037")
Most of the behavior in net/http can be mocked, extended, or altered. Although http.Client is a concrete type that implements HTTP client semantics, all of its fields are exported and may be customized. The Client.Transport field, in particular, may be replaced to make the Client do anything from using custom protocols (such as ftp:// or file://) to connecting directly to local handlers (without generating HTTP protocol bytes or sending anything over the network). The client functions, such as http.Get, all utilize the exported http.DefaultClient package variable (which you may modify), so code that utilizes these convenience functions does not, for example, have to be changed to call methods on a custom Client variable. Note that while it would be unreasonable to modify global behavior in a publicly-available library, it's very useful to do so in applications and tests (including library tests). http://play.golang.org/p/afljO086iB contains a custom http.RoundTripper that rewrites the request URL so that it'll be routed to a locally hosted httptest.Server, and another example that directly passes the request to an http.Handler, along with a custom http.ResponseWriter implementation, in order to create an http.Response. The second approach isn't as diligent as the first (it doesn't fill out as many fields in the Response value) but is more efficient, and should be compatible enough to work with most handlers and client callers. The above-linked code is included below as well: package main import ( "fmt" "io" "log" "net/http" "net/http/httptest" "net/url" "os" "path" "strings" ) func Handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "hello %s\n", path.Base(r.URL.Path)) } func main() { s := httptest.NewServer(http.HandlerFunc(Handler)) u, err := url.Parse(s.URL) if err != nil { log.Fatalln("failed to parse httptest.Server URL:", err) } http.DefaultClient.Transport = RewriteTransport{URL: u} resp, err := http.Get("https://google.com/path-one") if err != nil { log.Fatalln("failed to send first request:", err) } fmt.Println("[First Response]") resp.Write(os.Stdout) fmt.Print("\n", strings.Repeat("-", 80), "\n\n") http.DefaultClient.Transport = HandlerTransport{http.HandlerFunc(Handler)} resp, err = http.Get("https://google.com/path-two") if err != nil { log.Fatalln("failed to send second request:", err) } fmt.Println("[Second Response]") resp.Write(os.Stdout) } // RewriteTransport is an http.RoundTripper that rewrites requests // using the provided URL's Scheme and Host, and its Path as a prefix. // The Opaque field is untouched. // If Transport is nil, http.DefaultTransport is used type RewriteTransport struct { Transport http.RoundTripper URL *url.URL } func (t RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) { // note that url.URL.ResolveReference doesn't work here // since t.u is an absolute url req.URL.Scheme = t.URL.Scheme req.URL.Host = t.URL.Host req.URL.Path = path.Join(t.URL.Path, req.URL.Path) rt := t.Transport if rt == nil { rt = http.DefaultTransport } return rt.RoundTrip(req) } type HandlerTransport struct{ h http.Handler } func (t HandlerTransport) RoundTrip(req *http.Request) (*http.Response, error) { r, w := io.Pipe() resp := &http.Response{ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(http.Header), Body: r, Request: req, } ready := make(chan struct{}) prw := &pipeResponseWriter{r, w, resp, ready} go func() { defer w.Close() t.h.ServeHTTP(prw, req) }() <-ready return resp, nil } type pipeResponseWriter struct { r *io.PipeReader w *io.PipeWriter resp *http.Response ready chan<- struct{} } func (w *pipeResponseWriter) Header() http.Header { return w.resp.Header } func (w *pipeResponseWriter) Write(p []byte) (int, error) { if w.ready != nil { w.WriteHeader(http.StatusOK) } return w.w.Write(p) } func (w *pipeResponseWriter) WriteHeader(status int) { if w.ready == nil { // already called return } w.resp.StatusCode = status w.resp.Status = fmt.Sprintf("%d %s", status, http.StatusText(status)) close(w.ready) w.ready = nil }
The reason you're getting the error http: TLS handshake error from 127.0.0.1:45678: tls: oversized record received with length 20037 is because https requires a domain name (not an IP Address). Domain names are SSL certificates are assigned to. Start the httptest server in TLS mode with your own certs cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") if err != nil { log.Panic("bad server certs: ", err) } certs := []tls.Certificate{cert} server = httptest.NewUnstartedServer(router) server.TLS = &tls.Config{Certificates: certs} server.StartTLS() serverPort = ":" + strings.Split(server.URL, ":")[2] // it's always https://127.0.0.1:<port> server.URL = "https://sub.domain.com" + serverPort To provide a valid SSL certificate for a connection are the options of: Not supplying a cert and key Supplying a self-signed cert and key Supplying a real valid cert and key No Cert If you don't supply your own cert, then an example.com cert is loaded as default. Self-Signed Cert To create a testing cert can use the included self-signed cert generator at $GOROOT/src/crypto/tls/generate_cert.go --host "*.domain.name" You'll get x509: certificate signed by unknown authority warnings because it's self-signed so you'll need to have your client skip those warnings, by adding the following to your http.Transport field: TLSClientConfig: &tls.Config{InsecureSkipVerify: true} Valid Real Cert Finally, if you're going to use a real cert, then save the valid cert and key where they can be loaded. The key here is to use server.URL = https://sub.domain.com to supply your own domain.
From Go 1.9+ you can use func (s *Server) Client() *http.Client in the httptest package: Client returns an HTTP client configured for making requests to the server. It is configured to trust the server's TLS test certificate and will close its idle connections on Server.Close. Example from the package: package main import ( "fmt" "io" "log" "net/http" "net/http/httptest" ) func main() { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() client := ts.Client() res, err := client.Get(ts.URL) if err != nil { log.Fatal(err) } greeting, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Printf("%s", greeting) }