mirror of
https://github.com/kataras/iris.git
synced 2025-12-18 10:27:06 +00:00
first release of SSO package and more examples
This commit is contained in:
106
_examples/mvc/websocket-sso/browser/index.html
Normal file
106
_examples/mvc/websocket-sso/browser/index.html
Normal file
@@ -0,0 +1,106 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Online visitors MVC example</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, "San Francisco", "Helvetica Neue", "Noto", "Roboto", "Calibri Light", sans-serif;
|
||||
color: #212121;
|
||||
font-size: 1.0em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 750px;
|
||||
margin: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#online_visitors {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<span id="online_visitors">1 online visitor</span>
|
||||
</div>
|
||||
|
||||
<!-- the message's input -->
|
||||
<input id="input" type="text" />
|
||||
|
||||
<!-- when clicked then a websocket event will be sent to the server, at this example we registered the 'chat' -->
|
||||
<button id="sendBtn" disabled>Send</button>
|
||||
|
||||
<!-- the messages will be shown here -->
|
||||
<pre id="output"></pre>
|
||||
<!-- import the iris client-side library for browser from a CDN or locally.
|
||||
However, `neffos.(min.)js` is a NPM package too so alternatively,
|
||||
you can use it as dependency on your package.json and all nodejs-npm tooling become available:
|
||||
see the "browserify" example for more-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
const wsURL = "ws://localhost:8080/protected/ws"
|
||||
var outputTxt = document.getElementById("output");
|
||||
function addMessage(msg) {
|
||||
outputTxt.innerHTML += msg + "\n";
|
||||
}
|
||||
|
||||
async function runExample() {
|
||||
try {
|
||||
const conn = await neffos.dial(wsURL, {
|
||||
default: { // "default" namespace.
|
||||
_OnNamespaceConnected: function (nsConn, msg) {
|
||||
if (nsConn.conn.wasReconnected()) {
|
||||
addMessage("re-connected after " + nsConn.conn.reconnectTries.toString() + " trie(s)");
|
||||
}
|
||||
|
||||
let inputTxt = document.getElementById("input");
|
||||
let sendBtn = document.getElementById("sendBtn");
|
||||
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.onclick = function () {
|
||||
const input = inputTxt.value;
|
||||
inputTxt.value = "";
|
||||
nsConn.emit("OnChat", input);
|
||||
addMessage("Me: " + input);
|
||||
};
|
||||
|
||||
addMessage("connected to namespace: " + msg.Namespace);
|
||||
},
|
||||
_OnNamespaceDisconnect: function (nsConn, msg) {
|
||||
addMessage("disconnected from namespace: " + msg.Namespace);
|
||||
},
|
||||
OnChat: function (nsConn, msg) { // "OnChat" event.
|
||||
console.log(msg);
|
||||
|
||||
addMessage(msg.Body);
|
||||
},
|
||||
OnVisit: function (nsConn, msg) {
|
||||
const newCount = Number(msg.Body); // or parseInt.
|
||||
console.log("visit websocket event with newCount of: ", newCount);
|
||||
|
||||
var text = "1 online visitor";
|
||||
if (newCount > 1) {
|
||||
text = newCount + " online visitors";
|
||||
}
|
||||
document.getElementById("online_visitors").innerHTML = text;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
conn.connect("default");
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
runExample();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
72
_examples/mvc/websocket-sso/main.go
Normal file
72
_examples/mvc/websocket-sso/main.go
Normal file
@@ -0,0 +1,72 @@
|
||||
//go:build go1.18
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
"github.com/kataras/iris/v12/sso"
|
||||
"github.com/kataras/iris/v12/websocket"
|
||||
)
|
||||
|
||||
// $ go run .
|
||||
func main() {
|
||||
app := newApp()
|
||||
|
||||
// http://localhost:8080/signin (creds: kataras2006@hotmail.com 123456)
|
||||
// http://localhost:8080/protected
|
||||
// http://localhost:8080/signout
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func newApp() *iris.Application {
|
||||
app := iris.New()
|
||||
|
||||
// Auth part.
|
||||
app.RegisterView(iris.Blocks(iris.Dir("./views"), ".html").
|
||||
LayoutDir("layouts").
|
||||
Layout("main"))
|
||||
|
||||
s := sso.MustLoad[User]("./sso.yml")
|
||||
s.AddProvider(NewProvider())
|
||||
|
||||
app.Get("/signin", renderSigninForm)
|
||||
app.Post("/signin", s.SigninHandler)
|
||||
app.Get("/signout", s.SignoutHandler)
|
||||
//
|
||||
|
||||
websocketAPI := app.Party("/protected")
|
||||
websocketAPI.Use(s.VerifyHandler())
|
||||
websocketAPI.HandleDir("/", iris.Dir("./browser")) // render the ./browser/index.html.
|
||||
|
||||
websocketMVC := mvc.New(websocketAPI)
|
||||
websocketMVC.HandleWebsocket(new(websocketController))
|
||||
websocketServer := websocket.New(websocket.DefaultGorillaUpgrader, websocketMVC)
|
||||
websocketAPI.Get("/ws", s.VerifyHandler() /* optional */, websocket.Handler(websocketServer))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func renderSigninForm(ctx iris.Context) {
|
||||
ctx.View("signin", iris.Map{"Title": "Signin Page"})
|
||||
}
|
||||
|
||||
type websocketController struct {
|
||||
*websocket.NSConn `stateless:"true"`
|
||||
}
|
||||
|
||||
func (c *websocketController) Namespace() string {
|
||||
return "default"
|
||||
}
|
||||
|
||||
func (c *websocketController) OnChat(msg websocket.Message) error {
|
||||
ctx := websocket.GetContext(c.Conn)
|
||||
user := sso.GetUser[User](ctx)
|
||||
|
||||
msg.Body = []byte(fmt.Sprintf("%s: %s", user.Email, string(msg.Body)))
|
||||
c.Conn.Server().Broadcast(c, msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
32
_examples/mvc/websocket-sso/sso.yml
Normal file
32
_examples/mvc/websocket-sso/sso.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
Cookie: # optional.
|
||||
Name: "iris_sso"
|
||||
Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit).
|
||||
Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit).
|
||||
Keys:
|
||||
- ID: IRIS_SSO_ACCESS # required.
|
||||
Alg: EdDSA
|
||||
MaxAge: 2h # 2 hours lifetime for access tokens.
|
||||
Private: |+
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIFdZWoDdFny5SMnP9Fyfr8bafi/B527EVZh8JJjDTIFO
|
||||
-----END PRIVATE KEY-----
|
||||
Public: |+
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4=
|
||||
-----END PUBLIC KEY-----
|
||||
- ID: IRIS_SSO_REFRESH # optional. Good practise to have it though.
|
||||
Alg: EdDSA
|
||||
# 1 month lifetime for refresh tokens,
|
||||
# after that period the user has to signin again.
|
||||
MaxAge: 720h
|
||||
Private: |+
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIHJ1aoIjA2sRp5eqGjGR3/UMucrHbBdBv9p8uwfzZ1KZ
|
||||
-----END PRIVATE KEY-----
|
||||
Public: |+
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAsKKAr+kDtfAqwG7cZdoEAfh9jHt9W8qi9ur5AA1KQAQ=
|
||||
-----END PUBLIC KEY-----
|
||||
# Example of setting a binary form of the encryption key for refresh tokens,
|
||||
# it could be a "string" as well.
|
||||
EncryptionKey: !!binary stSNLTu91YyihPxzeEOXKwGVMG00CjcC/68G8nMgmqA=
|
||||
33
_examples/mvc/websocket-sso/user.go
Normal file
33
_examples/mvc/websocket-sso/user.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build go1.18
|
||||
|
||||
package main
|
||||
|
||||
type AccessRole uint16
|
||||
|
||||
func (r AccessRole) Is(v AccessRole) bool {
|
||||
return r&v != 0
|
||||
}
|
||||
|
||||
func (r AccessRole) Allow(v AccessRole) bool {
|
||||
return r&v >= v
|
||||
}
|
||||
|
||||
const (
|
||||
InvalidAccessRole AccessRole = 1 << iota
|
||||
Read
|
||||
Write
|
||||
Delete
|
||||
|
||||
Owner = Read | Write | Delete
|
||||
Member = Read | Write
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Role AccessRole `json:"role"`
|
||||
}
|
||||
|
||||
func (u User) GetID() string {
|
||||
return u.ID
|
||||
}
|
||||
100
_examples/mvc/websocket-sso/user_provider.go
Normal file
100
_examples/mvc/websocket-sso/user_provider.go
Normal file
@@ -0,0 +1,100 @@
|
||||
//go:build go1.18
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12/sso"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
dataset []User
|
||||
|
||||
invalidated map[string]struct{} // key = token. Entry is blocked.
|
||||
invalidatedAll map[string]int64 // key = user id, value = timestamp. Issued before is consider invalid.
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewProvider() *Provider {
|
||||
return &Provider{
|
||||
dataset: []User{
|
||||
{
|
||||
ID: "id-1",
|
||||
Email: "kataras2006@hotmail.com",
|
||||
Role: Owner,
|
||||
},
|
||||
{
|
||||
ID: "id-2",
|
||||
Email: "example@example.com",
|
||||
Role: Member,
|
||||
},
|
||||
},
|
||||
invalidated: make(map[string]struct{}),
|
||||
invalidatedAll: make(map[string]int64),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Signin(ctx context.Context, username, password string) (User, error) { // fired on SigninHandler.
|
||||
// your database...
|
||||
for _, user := range p.dataset {
|
||||
if user.Email == username {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
return User{}, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on VerifyHandler.
|
||||
// your database and checks of blocked tokens...
|
||||
|
||||
// check for specific token ids.
|
||||
p.mu.RLock()
|
||||
_, tokenBlocked := p.invalidated[standardClaims.ID]
|
||||
if !tokenBlocked {
|
||||
// this will disallow refresh tokens with issuer as the blocked access token as well.
|
||||
if standardClaims.Issuer != "" {
|
||||
_, tokenBlocked = p.invalidated[standardClaims.Issuer]
|
||||
}
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
if tokenBlocked {
|
||||
return fmt.Errorf("token was invalidated")
|
||||
}
|
||||
//
|
||||
|
||||
// check all tokens issuet before the "InvalidateToken" method was fired for this user.
|
||||
p.mu.RLock()
|
||||
ts, oldUserBlocked := p.invalidatedAll[u.ID]
|
||||
p.mu.RUnlock()
|
||||
|
||||
if oldUserBlocked && standardClaims.IssuedAt <= ts {
|
||||
return fmt.Errorf("token was invalidated")
|
||||
}
|
||||
//
|
||||
|
||||
return nil // else valid.
|
||||
}
|
||||
|
||||
func (p *Provider) InvalidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on SignoutHandler.
|
||||
// invalidate this specific token.
|
||||
p.mu.Lock()
|
||||
p.invalidated[standardClaims.ID] = struct{}{}
|
||||
p.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) InvalidateTokens(ctx context.Context, u User) error { // fired on SignoutAllHandler.
|
||||
// invalidate all previous tokens came from "u".
|
||||
p.mu.Lock()
|
||||
p.invalidatedAll[u.ID] = time.Now().Unix()
|
||||
p.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
30
_examples/mvc/websocket-sso/views/layouts/main.html
Normal file
30
_examples/mvc/websocket-sso/views/layouts/main.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}</title>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
main {
|
||||
display: block;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div class="container">
|
||||
<main>{{ template "content" . }}</main>
|
||||
<footer style="position: fixed; bottom: 0; width: 100%;">{{ partial "partials/footer" .}}</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1
_examples/mvc/websocket-sso/views/partials/footer.html
Normal file
1
_examples/mvc/websocket-sso/views/partials/footer.html
Normal file
@@ -0,0 +1 @@
|
||||
<i>Iris Web Framework © 2022</i>
|
||||
9
_examples/mvc/websocket-sso/views/signin.html
Normal file
9
_examples/mvc/websocket-sso/views/signin.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="user_signin">
|
||||
<form action="" method="post">
|
||||
<label for="username">Email:</label>
|
||||
<input name="username" type="email" />
|
||||
<label for="password">Password:</label>
|
||||
<input name="password" type="password" />
|
||||
<input type="submit" value="Sign in" />
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user