mirror of
https://github.com/wahyd4/loginsrv.git
synced 2026-08-09 04:46:29 +10:00
implemented logout feature
This commit is contained in:
@@ -81,6 +81,10 @@ Returns a simple bootstrap styled login form.
|
||||
The returned html follows the ui composition conventions from (lib-compose)[https://github.com/tarent/lib-compose],
|
||||
so it can be embedded into an existing layout.
|
||||
|
||||
### GET /login/<provider>
|
||||
|
||||
Starts the Oauth Web Flow with the configured provider. E.g. `GET /login/github` redirects to the github login form.
|
||||
|
||||
### POST /login
|
||||
|
||||
Does the login and returns the JWT. Depending on the content-type, and parameters a classical JSON-Rest or a redirect can be performed.
|
||||
@@ -108,6 +112,12 @@ Does the login and returns the JWT. Depending on the content-type, and parameter
|
||||
|
||||
Hint: The status `401 Unauthorized` is not used as a return code to not conflict with an Http BasicAuth Authentication.
|
||||
|
||||
### DELETE /login
|
||||
|
||||
Deletes the JWT Cookie.
|
||||
|
||||
For simple usage in web applications, this can also be called by `GET|POST /login?logout=true`
|
||||
|
||||
#### Example:
|
||||
Default is to return the token as Content-Type application/jwt within the body.
|
||||
```
|
||||
|
||||
+63
-19
@@ -10,6 +10,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const contentTypeHtml = "text/html; charset=utf-8"
|
||||
@@ -97,7 +98,7 @@ func (h *Handler) handleOauth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if !(r.Method == "GET" ||
|
||||
if !(r.Method == "GET" || r.Method == "DELETE" ||
|
||||
(r.Method == "POST" &&
|
||||
(strings.HasPrefix(contentType, "application/json") ||
|
||||
strings.HasPrefix(contentType, "application/x-www-form-urlencoded") ||
|
||||
@@ -107,11 +108,24 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
r.ParseForm()
|
||||
if r.Method == "GET" {
|
||||
if r.Method == "DELETE" || r.FormValue("logout") == "true" {
|
||||
h.deleteToken(w)
|
||||
writeLoginForm(w,
|
||||
map[string]interface{}{
|
||||
"path": r.URL.Path,
|
||||
"config": h.config,
|
||||
loginFormData{
|
||||
Path: r.URL.Path,
|
||||
Config: h.config,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
userInfo, valid := h.getToken(r)
|
||||
writeLoginForm(w,
|
||||
loginFormData{
|
||||
Path: r.URL.Path,
|
||||
Config: h.config,
|
||||
Authenticated: valid,
|
||||
UserInfo: userInfo,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -143,6 +157,17 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) deleteToken(w http.ResponseWriter) {
|
||||
cookie := &http.Cookie{
|
||||
Name: h.config.CookieName,
|
||||
Value: "delete",
|
||||
HttpOnly: true,
|
||||
Expires: time.Unix(0, 0),
|
||||
Path: "/",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, userInfo jwt.Claims) {
|
||||
token, err := h.createToken(userInfo)
|
||||
if err != nil {
|
||||
@@ -152,7 +177,12 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u
|
||||
}
|
||||
if wantHtml(r) {
|
||||
// TODO: set livetime
|
||||
cookie := &http.Cookie{Name: h.config.CookieName, Value: token, HttpOnly: true}
|
||||
cookie := &http.Cookie{
|
||||
Name: h.config.CookieName,
|
||||
Value: token,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
w.Header().Set("Location", h.config.SuccessUrl)
|
||||
w.WriteHeader(303)
|
||||
@@ -169,17 +199,32 @@ func (h *Handler) createToken(userInfo jwt.Claims) (string, error) {
|
||||
return token.SignedString([]byte(h.config.JwtSecret))
|
||||
}
|
||||
|
||||
func (h *Handler) getToken(r *http.Request) (userInfo UserInfo, valid bool) {
|
||||
c, err := r.Cookie(h.config.CookieName)
|
||||
if err != nil {
|
||||
return UserInfo{}, false
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(c.Value, &UserInfo{}, func(*jwt.Token) (interface{}, error) {
|
||||
return []byte(h.config.JwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return UserInfo{}, false
|
||||
}
|
||||
|
||||
u, v := token.Claims.(*UserInfo)
|
||||
return *u, v
|
||||
}
|
||||
|
||||
func (h *Handler) respondError(w http.ResponseWriter, r *http.Request) {
|
||||
if wantHtml(r) {
|
||||
w.Header().Set("Content-Type", contentTypeHtml)
|
||||
w.WriteHeader(500)
|
||||
username, _, _ := getCredentials(r)
|
||||
writeLoginForm(w,
|
||||
map[string]interface{}{
|
||||
"path": r.URL.Path,
|
||||
"error": true,
|
||||
"config": h.config,
|
||||
"username": username,
|
||||
loginFormData{
|
||||
Path: r.URL.Path,
|
||||
Error: true,
|
||||
Config: h.config,
|
||||
UserInfo: UserInfo{Username: username},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -199,12 +244,11 @@ func (h *Handler) respondAuthFailure(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(403)
|
||||
username, _, _ := getCredentials(r)
|
||||
writeLoginForm(w,
|
||||
map[string]interface{}{
|
||||
"path": r.URL.Path,
|
||||
"failure": true,
|
||||
"config": h.config,
|
||||
|
||||
"username": username,
|
||||
loginFormData{
|
||||
Path: r.URL.Path,
|
||||
Failure: true,
|
||||
Config: h.config,
|
||||
UserInfo: UserInfo{Username: username},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ func TestHandler_LoginWeb(t *testing.T) {
|
||||
claims, err := tokenAsMap(strings.SplitN(headerParts[1], ";", 2)[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]interface{}{"sub": "bob"}, claims)
|
||||
assert.Contains(t, headerParts[1]+";", "Path=/;")
|
||||
|
||||
// show the login form again after authentication failed
|
||||
recorder = call(req("POST", "/context/login", "username=bob&password=FOOBAR", TypeForm, AcceptHtml))
|
||||
@@ -119,6 +120,23 @@ func TestHandler_LoginWeb(t *testing.T) {
|
||||
assert.Equal(t, recorder.Header().Get("Set-Cookie"), "")
|
||||
}
|
||||
|
||||
func TestHandler_Logout(t *testing.T) {
|
||||
// DELETE
|
||||
recorder := call(req("DELETE", "/context/login", ""))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
|
||||
|
||||
// GET + param
|
||||
recorder = call(req("GET", "/context/login?logout=true", ""))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
|
||||
|
||||
// POST + param
|
||||
recorder = call(req("POST", "/context/login", "logout=true", TypeForm))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
|
||||
}
|
||||
|
||||
func TestHandler_LoginError(t *testing.T) {
|
||||
h := testHandlerWithError()
|
||||
|
||||
@@ -142,6 +160,51 @@ func TestHandler_LoginError(t *testing.T) {
|
||||
assert.Contains(t, recorder.Body.String(), "Internal Error")
|
||||
}
|
||||
|
||||
func TestHandler_getToken_Valid(t *testing.T) {
|
||||
h := testHandler()
|
||||
input := UserInfo{Username: "marvin"}
|
||||
token, err := h.createToken(input)
|
||||
assert.NoError(t, err)
|
||||
r := &http.Request{
|
||||
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
|
||||
}
|
||||
userInfo, valid := h.getToken(r)
|
||||
assert.True(t, valid)
|
||||
assert.Equal(t, input, userInfo)
|
||||
}
|
||||
|
||||
func TestHandler_getToken_InvalidSecret(t *testing.T) {
|
||||
h := testHandler()
|
||||
input := UserInfo{Username: "marvin"}
|
||||
token, err := h.createToken(input)
|
||||
assert.NoError(t, err)
|
||||
r := &http.Request{
|
||||
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
|
||||
}
|
||||
|
||||
// modify secret
|
||||
h.config.JwtSecret = "foobar"
|
||||
|
||||
_, valid := h.getToken(r)
|
||||
assert.False(t, valid)
|
||||
}
|
||||
|
||||
func TestHandler_getToken_InvalidToken(t *testing.T) {
|
||||
h := testHandler()
|
||||
r := &http.Request{
|
||||
Header: http.Header{"Cookie": {h.config.CookieName + "=asdcsadcsadc"}},
|
||||
}
|
||||
|
||||
_, valid := h.getToken(r)
|
||||
assert.False(t, valid)
|
||||
}
|
||||
|
||||
func TestHandler_getToken_InvalidNoToken(t *testing.T) {
|
||||
h := testHandler()
|
||||
_, valid := h.getToken(&http.Request{})
|
||||
assert.False(t, valid)
|
||||
}
|
||||
|
||||
func testHandler() *Handler {
|
||||
return &Handler{
|
||||
backends: []Backend{
|
||||
|
||||
+102
-33
@@ -1,54 +1,123 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/tarent/lib-compose/logging"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const loginForm = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link uic-remove rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
<link uic-remove rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.1.1/bootstrap-social.min.css">
|
||||
<link uic-remove rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">
|
||||
<style>
|
||||
.vertical-offset-100{
|
||||
padding-top:100px;
|
||||
}
|
||||
.vertical-offset-100{
|
||||
padding-top:100px;
|
||||
}
|
||||
.login-or-container {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
clear: both;
|
||||
color: #6a737c;
|
||||
font-variant: small-caps;
|
||||
}
|
||||
.login-or-hr {
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
top: 28px;
|
||||
height: 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #e4e6e8;
|
||||
}
|
||||
.login-or {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
background-color: #FFF;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<uic-fragment name="content">
|
||||
<div class="container">
|
||||
<div class="row vertical-offset-100">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Please sign in</h3>
|
||||
{{ if .error}}Internal Error. Please try again later{{end}}
|
||||
{{ if .failure}}Wrong credentials{{end}}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form accept-charset="UTF-8" role="form" method="POST" action="{{.path}}">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Username" name="username" value="{{.username}}" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Password" name="password" type="password" value="">
|
||||
</div>
|
||||
<input class="btn btn-lg btn-success btn-block" type="submit" value="Login">
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row vertical-offset-100">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
|
||||
{{ if .Error}}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong>Internal Error. </strong> Please try again later.
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{ if .Authenticated}}
|
||||
<h3>Welcome {{.UserInfo.Username}}</h3>
|
||||
<a href="login?logout=true">Logout</a>
|
||||
{{else}}
|
||||
<a class="btn btn-block btn-lg btn-social btn-github" href="login/github">
|
||||
<span class="fa fa-github"></span> Sign in with Github
|
||||
</a>
|
||||
<div class="login-or-container">
|
||||
<hr class="login-or-hr">
|
||||
<div class="login-or lead">or</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
<div class="panel-title">
|
||||
<h4>Sign in</h4>
|
||||
{{ if .Failure}}<div class="alert alert-warning" role="alert">Invalid credentials</div>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form accept-charset="UTF-8" role="form" method="POST" action="{{.Path}}">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Username" name="username" value="{{.UserInfo.Username}}" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Password" name="password" type="password" value="">
|
||||
</div>
|
||||
<input class="btn btn-lg btn-success btn-block" type="submit" value="Login">
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</uic-fragment>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
</html>`
|
||||
|
||||
func writeLoginForm(w io.Writer, params map[string]interface{}) {
|
||||
type loginFormData struct {
|
||||
Path string
|
||||
Error bool
|
||||
Failure bool
|
||||
Config *Config
|
||||
Authenticated bool
|
||||
UserInfo UserInfo
|
||||
}
|
||||
|
||||
func writeLoginForm(w http.ResponseWriter, params loginFormData) {
|
||||
t := template.Must(template.New("loginForm").Parse(loginForm))
|
||||
t.Execute(w, params)
|
||||
b := bytes.NewBuffer(nil)
|
||||
err := t.Execute(b, params)
|
||||
if err != nil {
|
||||
logging.Logger.WithError(err).Error()
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(`Internal Server Error`))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentTypeHtml)
|
||||
if params.Error {
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
|
||||
w.Write(b.Bytes())
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,6 +13,6 @@ var providerGithub = Provider{
|
||||
GetUserInfo: func(token TokenInfo) (map[string]string, error) {
|
||||
//http.Get(fmt.Sprintf("%v/user?access_token=%v"), githubApi, token.AccessToken)
|
||||
// https://developer.github.com/v3/users/
|
||||
return nil, nil
|
||||
return map[string]string{"username": "demo"}, nil
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user