1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 10:27:06 +00:00

rename the sso to auth package

This commit is contained in:
Gerasimos (Makis) Maropoulos
2022-04-02 17:30:55 +03:00
parent 60e19de9e2
commit 8652ee09f6
24 changed files with 233 additions and 205 deletions

View File

@@ -0,0 +1,35 @@
Headers: # required.
- "Authorization"
- "X-Authorization"
Cookie: # optional.
Name: "iris_auth_cookie"
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_AUTH_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_AUTH_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=

View 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>

View File

@@ -0,0 +1,72 @@
//go:build go1.18
package main
import (
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/auth"
"github.com/kataras/iris/v12/mvc"
"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 := auth.MustLoad[User]("./auth.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 := auth.GetUser[User](ctx)
msg.Body = []byte(fmt.Sprintf("%s: %s", user.Email, string(msg.Body)))
c.Conn.Server().Broadcast(c, msg)
return nil
}

View 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
}

View File

@@ -0,0 +1,100 @@
//go:build go1.18
package main
import (
"context"
"fmt"
"sync"
"time"
"github.com/kataras/iris/v12/auth"
)
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 auth.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 auth.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
}

View 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>

View File

@@ -0,0 +1 @@
<i>Iris Web Framework &copy; 2022</i>

View 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>