Repository: go-openapi/runtime (go)

Issue:
API UI middleware oAuth2 callback not working
For my project i'm using go-openapi/runtime to host api documents in my project, but run into some issues trying to add oauth2.
I tried both swagger and rapidoc and both give a 404 after redirecting back from the authentication.

API doc initialization:
https://github.com/deb-ict/cloudbm-community/blob/features/authentication/cmd/webhost/main.go
```
	contactSwaggerOptions := middleware.SwaggerUIOpts{
		BasePath: "/api/contact",
		Path:     "swagger",
		SpecURL:  "/api/spec/contacts.yaml",
	}
	contactSwaggerHandler := middleware.SwaggerUI(contactSwaggerOptions, nil)
	router.Handle("/api/contact/swagger", contactSwaggerHandler)
```
```
	contactRapidocOptions := middleware.RapiDocOpts{
		BasePath: "/api/contact",
		Path:     "docs",
		SpecURL:  "/api/spec/contacts.yaml",
	}
	contactRapidocHandler := middleware.RapiDoc(contactRapidocOptions, nil)
	router.Handle("/api/contact/docs", contactRapidocHandler)
```

API spec: (generated by go-swagger)
https://github.com/deb-ict/cloudbm-community/blob/features/authentication/api/contacts.yaml
```
securityDefinitions:
    oauth:
        authorizationUrl: /auth/v1/authorize
        flow: implicit
        scopes:
            contacts.metadata.read: Read contacts metadata
            contacts.metadata.write: Write contacts metadata
            contacts.read: Read company and contact person information
            contacts.write: Write company and contact person information
        tokenUrl: /auth/v1/token
        type: oauth2
```

For swagger, after authentication, i get a 404 Page not found error on `oauth2-redirect.html`
Callback url:
`http://localhost:8080/api/contact/oauth2-redirect.html?access_token=mytoken&expires_in=3600&state=TW9uIE9jdCAxNyAyMDIyIDIzOjI0OjU2IEdNVCswMjAwIChDZW50cmFsIEV1cm9wZWFuIFN1bW1lciBUaW1lKQ%3D%3D&token_type=bearer`

For rapidoc, after authentication, i get a 404 Page not found on `oauth-receiver.html`
Callback url:
`http://localhost:8080/api/contact/oauth-receiver.html?access_token=mytoken&expires_in=3600&state=x2kzyce&token_type=bearer
`

As for rapidoc, the authentication endpoints goes to `/api/contact/auth/v1/authorize` instead of `/auth/v1/authorize` like swagger does

Pull request:
Add swagger oath2 callback
Implemented swagger oauth2 callback page to have working oauth2 authentication flow on swaggerUI middleware.

Fixes #255 

Example usage:
```
swaggerOptions := middleware.SwaggerUIOpts{}
swaggerHandler := middleware.SwaggerUI(swaggerOptions, nil)
swaggerOAuthHandler := middleware.SwaggerUIOAuth(swaggerOptions, nil)
router.Handle("/docs", swaggerHandler)
router.Handle("/docs/oauth2-callback", swaggerOAuthHandler)
```


Patch:
diff --git a/middleware/swaggerui.go b/middleware/swaggerui.go
index bab523c..b4dea29 100644
--- a/middleware/swaggerui.go
+++ b/middleware/swaggerui.go
@@ -16,6 +16,8 @@ type SwaggerUIOpts struct {
 	Path string
 	// SpecURL the url to find the spec for
 	SpecURL string
+	// OAuthCallbackURL the url called after OAuth2 login
+	OAuthCallbackURL string
 
 	// The three components needed to embed swagger-ui
 	SwaggerURL       string
@@ -40,6 +42,9 @@ func (r *SwaggerUIOpts) EnsureDefaults() {
 	if r.SpecURL == "" {
 		r.SpecURL = "/swagger.json"
 	}
+	if r.OAuthCallbackURL == "" {
+		r.OAuthCallbackURL = path.Join(r.BasePath, r.Path, "oauth2-callback")
+	}
 	if r.SwaggerURL == "" {
 		r.SwaggerURL = swaggerLatest
 	}
@@ -149,7 +154,8 @@ const (
         plugins: [
           SwaggerUIBundle.plugins.DownloadUrl
         ],
-        layout: "StandaloneLayout"
+        layout: "StandaloneLayout",
+		oauth2RedirectUrl: '{{ .OAuthCallbackURL }}'
       })
       // End Swagger UI call region
 
diff --git a/middleware/swaggerui_oauth2.go b/middleware/swaggerui_oauth2.go
new file mode 100644
index 0000000..576f600
--- /dev/null
+++ b/middleware/swaggerui_oauth2.go
@@ -0,0 +1,122 @@
+package middleware
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"path"
+	"text/template"
+)
+
+func SwaggerUIOAuth2Callback(opts SwaggerUIOpts, next http.Handler) http.Handler {
+	opts.EnsureDefaults()
+
+	pth := opts.OAuthCallbackURL
+	tmpl := template.Must(template.New("swaggeroauth").Parse(swaggerOAuthTemplate))
+
+	buf := bytes.NewBuffer(nil)
+	_ = tmpl.Execute(buf, &opts)
+	b := buf.Bytes()
+
+	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		if path.Join(r.URL.Path) == pth {
+			rw.Header().Set("Content-Type", "text/html; charset=utf-8")
+			rw.WriteHeader(http.StatusOK)
+
+			_, _ = rw.Write(b)
+			return
+		}
+
+		if next == nil {
+			rw.Header().Set("Content-Type", "text/plain")
+			rw.WriteHeader(http.StatusNotFound)
+			_, _ = rw.Write([]byte(fmt.Sprintf("%q not found", pth)))
+			return
+		}
+		next.ServeHTTP(rw, r)
+	})
+}
+
+const (
+	swaggerOAuthTemplate = `
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>{{ .Title }}</title>
+</head>
+<body>
+<script>
+    'use strict';
+    function run () {
+        var oauth2 = window.opener.swaggerUIRedirectOauth2;
+        var sentState = oauth2.state;
+        var redirectUrl = oauth2.redirectUrl;
+        var isValid, qp, arr;
+
+        if (/code|token|error/.test(window.location.hash)) {
+            qp = window.location.hash.substring(1).replace('?', '&');
+        } else {
+            qp = location.search.substring(1);
+        }
+
+        arr = qp.split("&");
+        arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
+        qp = qp ? JSON.parse('{' + arr.join() + '}',
+                function (key, value) {
+                    return key === "" ? value : decodeURIComponent(value);
+                }
+        ) : {};
+
+        isValid = qp.state === sentState;
+
+        if ((
+          oauth2.auth.schema.get("flow") === "accessCode" ||
+          oauth2.auth.schema.get("flow") === "authorizationCode" ||
+          oauth2.auth.schema.get("flow") === "authorization_code"
+        ) && !oauth2.auth.code) {
+            if (!isValid) {
+                oauth2.errCb({
+                    authId: oauth2.auth.name,
+                    source: "auth",
+                    level: "warning",
+                    message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
+                });
+            }
+
+            if (qp.code) {
+                delete oauth2.state;
+                oauth2.auth.code = qp.code;
+                oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
+            } else {
+                let oauthErrorMsg;
+                if (qp.error) {
+                    oauthErrorMsg = "["+qp.error+"]: " +
+                        (qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
+                        (qp.error_uri ? "More info: "+qp.error_uri : "");
+                }
+
+                oauth2.errCb({
+                    authId: oauth2.auth.name,
+                    source: "auth",
+                    level: "error",
+                    message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
+                });
+            }
+        } else {
+            oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
+        }
+        window.close();
+    }
+
+    if (document.readyState !== 'loading') {
+        run();
+    } else {
+        document.addEventListener('DOMContentLoaded', function () {
+            run();
+        });
+    }
+</script>
+</body>
+</html>
+`
+)


Test patch:
diff --git a/middleware/swaggerui_oauth2_test.go b/middleware/swaggerui_oauth2_test.go
new file mode 100644
index 0000000..eab1639
--- /dev/null
+++ b/middleware/swaggerui_oauth2_test.go
@@ -0,0 +1,20 @@
+package middleware
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSwaggerUIOAuth2CallbackMiddleware(t *testing.T) {
+	redoc := SwaggerUIOAuth2Callback(SwaggerUIOpts{}, nil)
+
+	req, _ := http.NewRequest("GET", "/docs/oauth2-callback", nil)
+	recorder := httptest.NewRecorder()
+	redoc.ServeHTTP(recorder, req)
+	assert.Equal(t, 200, recorder.Code)
+	assert.Equal(t, "text/html; charset=utf-8", recorder.Header().Get("Content-Type"))
+	assert.Contains(t, recorder.Body.String(), "<title>API documentation</title>")
+}