├── .gitignore
├── api.http
├── authentication-flow
├── Dockerfile
├── authorization-code-flow
│ ├── REAMDE.md
│ ├── authentication-code.png
│ ├── authentication-code.wsd
│ └── src
│ │ ├── index.ts
│ │ └── replay-attack.ts
├── docker-compose.yaml
├── hybrid-flow
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── README.md
│ ├── hybrid-flow.png
│ ├── hybrid-flow.wsd
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── Admin.tsx
│ │ ├── App.tsx
│ │ ├── AuthProvider.tsx
│ │ ├── Callback.tsx
│ │ ├── Login.tsx
│ │ ├── Logout.tsx
│ │ ├── PrivateRoute.tsx
│ │ ├── main.tsx
│ │ ├── utils.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── implicit-flow
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── README.md
│ ├── implicit-flow.png
│ ├── implicit-flow.wsd
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── Admin.tsx
│ │ ├── App.tsx
│ │ ├── AuthProvider.tsx
│ │ ├── Callback.tsx
│ │ ├── Login.tsx
│ │ ├── Logout.tsx
│ │ ├── PrivateRoute.tsx
│ │ ├── main.tsx
│ │ ├── utils.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── package-lock.json
├── package.json
├── resource-owner-password-credentials
│ ├── README.md
│ ├── resource.owner-password-credentials.png
│ ├── resource.owner-password-credentials.wsd
│ └── src
│ │ ├── index.ts
│ │ └── login.html
└── tsconfig.json
└── keycloak
└── docker-compose.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | dbdata/
2 | node_modules/
3 | .history/
--------------------------------------------------------------------------------
/api.http:
--------------------------------------------------------------------------------
1 | ### Gerando access token (Direct Grant ou Resource Owner Password Credentials Grant)
2 | POST http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/token
3 | Content-Type: application/x-www-form-urlencoded
4 |
5 | username=user@user.com
6 | &password=secret
7 | &grant_type=password
8 | &client_id=fullcycle-client
9 |
10 |
11 | ### Gerando access token e id token
12 | POST http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/token
13 | Content-Type: application/x-www-form-urlencoded
14 |
15 | username=user@user.com
16 | &password=secret
17 | &grant_type=password
18 | &client_id=fullcycle-client
19 | &scope=openid
20 |
21 | ### authorization code flow
22 | POST http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/token
23 | Content-Type: application/x-www-form-urlencoded
24 |
25 | grant_type=authorization_code
26 | &client_id=fullcycle-client
27 | &code=d803812a-1cce-4d38-9d12-a33f143f5ee0.48ce3bf7-ef07-4aab-bf41-5b7726c97e56.34b28fed-8773-41a4-bfde-bf98c8950b26
28 | &redirect_uri=http://localhost:3000/callback
--------------------------------------------------------------------------------
/authentication-flow/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-slim
2 |
3 | WORKDIR /home/node/app
4 |
5 | USER node
6 |
7 | CMD ["tail", "-f", "/dev/null"]
--------------------------------------------------------------------------------
/authentication-flow/authorization-code-flow/REAMDE.md:
--------------------------------------------------------------------------------
1 | # Authorization Code Flow
2 |
3 | O fluxo Authorization Code Flow é geralmente recomendado para aplicativos web e aplicativos móveis confiáveis. Ele fornece um alto nível de segurança ao lidar com autorização e autenticação de usuários em um servidor de recursos.
4 |
5 | Este fluxo é particularmente adequado quando você possui um servidor back-end seguro que pode proteger seu segredo de cliente (client secret). No fluxo Authorization Code, o aplicativo solicita ao servidor de autorização (authorization server) um código de autorização, que é então trocado por um token de acesso. Essa troca ocorre no servidor back-end, onde as credenciais confidenciais podem ser armazenadas com segurança e a comunicação é realizada por meio de chamadas de back-end.
6 |
7 | O fluxo Authorization Code Flow é mais seguro do que o fluxo Implicit Flow, pois o token de acesso não é exposto diretamente no navegador do usuário. Além disso, ele permite a renovação de tokens de acesso sem exigir que o usuário forneça novamente suas credenciais de autenticação.
8 |
9 | Em resumo, o fluxo Authorization Code Flow é recomendado para aplicativos web e móveis confiáveis, nos quais a segurança é uma preocupação essencial e existe um servidor back-end seguro para gerenciar as interações com o servidor de autorização.
10 |
11 | 
12 |
13 | ## Single Sign On
14 |
15 | Similar ao Implicit Flow, o Authorization Code Flow também suporta Single Sign On (SSO). O SSO permite que um usuário faça login em um aplicativo e seja autenticado em outros aplicativos automaticamente, sem precisar fornecer suas credenciais novamente.
16 |
17 | ## Attacks
18 |
19 | ### Nonce (number used once) Replay Attack
20 |
21 | O ataque Nonce Replay ocorre quando um invasor intercepta um token de ID e o reutiliza para obter acesso a recursos protegidos. Para evitar esse ataque, o servidor de recursos deve verificar se o nonce no token de ID corresponde ao nonce no token de acesso.
22 |
23 | O nonce é um parâmetro usado para evitar ataques de repetição (replay attacks) em protocolos de autenticação baseados em tokens, como o OAuth 2.0 e o OpenID Connect. O termo "nonce" significa "number used once" (número usado apenas uma vez), indicando que o valor do parâmetro deve ser único para cada solicitação de autenticação.
24 |
25 | ### State Parameter CSRF Attack (Cross-Site Request Forgery)
26 |
27 | O ataque CSRF ocorre quando um invasor engana um usuário para que ele execute uma ação em um aplicativo web sem o conhecimento do usuário. Para evitar esse ataque, o aplicativo deve usar um parâmetro de estado (state parameter) para verificar se a resposta recebida do servidor de autorização corresponde à solicitação original.
28 |
29 | O state é um valor aleatório gerado pelo cliente antes de enviar a solicitação de autenticação e incluído na solicitação. O servidor de autenticação inclui o mesmo valor do state na resposta de autenticação, permitindo que o cliente verifique se a resposta é uma resposta legítima à solicitação original. Isso ajuda a evitar ataques de CSRF, em que um invasor engana um usuário para enviar uma solicitação maliciosa sem o conhecimento do usuário.
30 |
31 |
--------------------------------------------------------------------------------
/authentication-flow/authorization-code-flow/authentication-code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devfullcycle/fc-keycloak/006da5f1d9bedacfbcfe88143789b82ea28e0546/authentication-flow/authorization-code-flow/authentication-code.png
--------------------------------------------------------------------------------
/authentication-flow/authorization-code-flow/authentication-code.wsd:
--------------------------------------------------------------------------------
1 | @startuml
2 | actor "Resource Owner"
3 | participant "User Agent" as UA
4 | participant "Authorization Server" as AS
5 | participant "Client Application" as CA
6 | participant "Resource Server" as RS
7 |
8 | "Resource Owner" -> UA: Acessa a aplicação do cliente
9 | UA -> CA: Redireciona para a URL de autorização do AS
10 | CA -> AS: Requisita uma autorização de acesso ao RS
11 | AS -> "Resource Owner": Exibe a tela de login
12 | "Resource Owner" -> AS: Fornece as credenciais de autenticação
13 | AS -> "Resource Owner": Exibe a tela de consentimento de acesso ao RS
14 | "Resource Owner" -> AS: Concede o consentimento
15 | AS -> CA: Retorna um código de autorização
16 | CA -> AS: Requisita um token de acesso, fornecendo o código de autorização
17 | AS -> CA: Retorna um token de acesso
18 | CA -> RS: Requisita um recurso protegido, fornecendo o token de acesso
19 | RS --> CA: Retorna o recurso protegido
20 | @enduml
--------------------------------------------------------------------------------
/authentication-flow/authorization-code-flow/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import session from "express-session";
3 | import jwt from "jsonwebtoken";
4 | import crypto from "crypto";
5 |
6 | const app = express();
7 |
8 | const memoryStore = new session.MemoryStore();
9 |
10 | app.use(
11 | session({
12 | secret: "my-secret",
13 | resave: false,
14 | saveUninitialized: false,
15 | store: memoryStore,
16 | //expires
17 | })
18 | );
19 |
20 | const middlewareIsAuth = (
21 | req: express.Request,
22 | res: express.Response,
23 | next: express.NextFunction
24 | ) => {
25 | //@ts-expect-error - type mismatch
26 | if (!req.session.user) {
27 | return res.redirect("/login");
28 | }
29 | next();
30 | };
31 |
32 | app.get("/login", (req, res) => {
33 | const nonce = crypto.randomBytes(16).toString("base64");
34 | const state = crypto.randomBytes(16).toString("base64");
35 |
36 | //@ts-expect-error - type mismatch
37 | req.session.nonce = nonce;
38 | //@ts-expect-error - type mismatch
39 | req.session.state = state;
40 | req.session.save();
41 |
42 | // valor aleatório - sessão de usuário
43 | const loginParams = new URLSearchParams({
44 | client_id: "fullcycle-client",
45 | redirect_uri: "http://localhost:3000/callback",
46 | response_type: "code",
47 | scope: "openid",
48 | nonce,
49 | state
50 | });
51 |
52 | const url = `http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/auth?${loginParams.toString()}`;
53 | console.log(url);
54 | res.redirect(url);
55 | });
56 |
57 | app.get("/logout", (req, res) => {
58 | const logoutParams = new URLSearchParams({
59 | //client_id: "fullcycle-client",
60 | //@ts-expect-error
61 | id_token_hint: req.session.id_token,
62 | post_logout_redirect_uri: "http://localhost:3000/login",
63 | });
64 |
65 | req.session.destroy((err) => {
66 | console.error(err);
67 | });
68 |
69 | const url = `http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/logout?${logoutParams.toString()}`;
70 | res.redirect(url);
71 | });
72 | // /login ----> keycloak (formulario de auth) ----> /callback?code=123 ---> keycloak (devolve o token)
73 | //
74 | app.get("/callback", async (req, res) => {
75 | //@ts-expect-error - type mismatch
76 | if (req.session.user) {
77 | return res.redirect("/admin");
78 | }
79 |
80 | //@ts-expect-error - type mismatch
81 | if(req.query.state !== req.session.state) {
82 | //poderia redirecionar para o login em vez de mostrar o erro
83 | return res.status(401).json({ message: "Unauthenticated" });
84 | }
85 |
86 | console.log(req.query);
87 |
88 | const bodyParams = new URLSearchParams({
89 | client_id: "fullcycle-client",
90 | grant_type: "authorization_code",
91 | code: req.query.code as string,
92 | redirect_uri: "http://localhost:3000/callback",
93 | });
94 |
95 | const url = `http://host.docker.internal:8080/realms/fullcycle-realm/protocol/openid-connect/token`;
96 |
97 | const response = await fetch(url, {
98 | method: "POST",
99 | headers: {
100 | "Content-Type": "application/x-www-form-urlencoded",
101 | },
102 | body: bodyParams.toString(),
103 | });
104 |
105 | const result = await response.json();
106 |
107 | console.log(result);
108 | const payloadAccessToken = jwt.decode(result.access_token) as any;
109 | const payloadRefreshToken = jwt.decode(result.refresh_token) as any;
110 | const payloadIdToken = jwt.decode(result.id_token) as any;
111 |
112 | if (
113 | //@ts-expect-error - type mismatch
114 | payloadAccessToken!.nonce !== req.session.nonce ||
115 | //@ts-expect-error - type mismatch
116 | payloadRefreshToken.nonce !== req.session.nonce ||
117 | //@ts-expect-error - type mismatch
118 | payloadIdToken.nonce !== req.session.nonce
119 | ) {
120 | return res.status(401).json({ message: "Unauthenticated" });
121 | }
122 |
123 | console.log(payloadAccessToken);
124 | //@ts-expect-error - type mismatch
125 | req.session.user = payloadAccessToken;
126 | //@ts-expect-error - type mismatch
127 | req.session.access_token = result.access_token;
128 | //@ts-expect-error - type mismatch
129 | req.session.id_token = result.id_token;
130 | req.session.save();
131 | res.json(result);
132 | });
133 |
134 | app.get("/admin", middlewareIsAuth, (req, res) => {
135 | //@ts-expect-error - type mismatch
136 | res.json(req.session.user);
137 | });
138 |
139 | app.listen(3000, () => {
140 | console.log("Listening on port 3000");
141 | });
142 |
--------------------------------------------------------------------------------
/authentication-flow/authorization-code-flow/src/replay-attack.ts:
--------------------------------------------------------------------------------
1 | const url =
2 | "http://host.docker.internal:3000/callback?session_state=3e3447bc-55c8-4283-97d1-fdd2da0f5cd7&code=9ea4cd51-b214-4a1a-a52f-0e9e5ad21c39.3e3447bc-55c8-4283-97d1-fdd2da0f5cd7.34b28fed-8773-41a4-bfde-bf98c8950b26";
3 |
4 | const request1 = fetch(url);
5 | const request2 = fetch(url);
6 |
7 | Promise.all([request1, request2])
8 | .then(async (responses) => {
9 | return Promise.all(responses.map((response) => response.json()));
10 | })
11 | .then((jsons) => console.log(jsons));
12 |
--------------------------------------------------------------------------------
/authentication-flow/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | app:
6 | build: .
7 | ports:
8 | - 3000:3000
9 | volumes:
10 | - .:/home/node/app
11 | extra_hosts:
12 | - "host.docker.internal:172.17.0.1"
--------------------------------------------------------------------------------
/authentication-flow/hybrid-flow/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:react-hooks/recommended',
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
10 | plugins: ['react-refresh'],
11 | rules: {
12 | 'react-refresh/only-export-components': 'warn',
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/authentication-flow/hybrid-flow/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/authentication-flow/hybrid-flow/README.md:
--------------------------------------------------------------------------------
1 | # Hybrid Flow
2 |
3 | No Hybrid Flow, a primeira parte do fluxo é semelhante ao fluxo Authorization Code Flow, onde o cliente solicita uma autorização para o servidor de autorização e recebe um código de autorização. No entanto, ao contrário do Authorization Code Flow, no Hybrid Flow, o cliente também recebe o access token imediatamente após a troca do código de autorização, antes de fazer uma segunda chamada ao servidor de autorização.
4 |
5 | O risco de segurança no Hybrid Flow está na revelação prematura do access token, pois ele é retornado diretamente ao cliente após a troca do código de autorização, antes de qualquer interação com o servidor. Isso significa que o token é exposto no front-end, onde potenciais vulnerabilidades podem ser exploradas para obter acesso não autorizado ao token.
6 |
7 | O Hybrid Flow é mais adequado para aplicativos front-end JavaScript que interagem diretamente com o servidor de autorização. No entanto, é importante estar ciente das limitações de segurança associadas a esse fluxo e implementar as medidas apropriadas para proteger os tokens de acesso.
8 |
9 | ## Single Sign On
10 |
11 | Similar ao Authorization Code Flow e Implicit Flow, o Hybrid Flow também suporta Single Sign On (SSO). O SSO permite que um usuário faça login em um aplicativo e seja autenticado em outros aplicativos automaticamente, sem precisar fornecer suas credenciais novamente.
12 |
13 | ## Configuração
14 |
15 | response_type=code token ou response_type=code token id_token
16 |
17 | ## A hashtag é acrescentada ao redirecionamento
18 |
19 | http://localhost:3000/callback#state=YKWruxpK9QzU3vwOi5IxWg%3D%3D&session_state=593006ef-0a13-4472-bfef-5f4776fe3441&code=XXXXX&access_token=XXXXX&id_token=XXXXX&token_type=Bearer&expires_in=900
--------------------------------------------------------------------------------
/authentication-flow/hybrid-flow/hybrid-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devfullcycle/fc-keycloak/006da5f1d9bedacfbcfe88143789b82ea28e0546/authentication-flow/hybrid-flow/hybrid-flow.png
--------------------------------------------------------------------------------
/authentication-flow/hybrid-flow/hybrid-flow.wsd:
--------------------------------------------------------------------------------
1 | @startuml
2 |
3 | actor User
4 | participant UserAgent
5 | participant Client
6 | participant AuthorizationServer
7 | participant ResourceServer
8 |
9 | User -> UserAgent: Acessa o aplicativo
10 | UserAgent -> Client: Solicita autorização
11 | Client -> AuthorizationServer: Solicita autorização
12 | AuthorizationServer -> UserAgent: Exibe tela de login
13 | UserAgent -> AuthorizationServer: Insere credenciais
14 | AuthorizationServer -> UserAgent: Autentica o usuário
15 | UserAgent -> AuthorizationServer: Autoriza o acesso
16 | AuthorizationServer -> UserAgent: Retorna o código de autorização e o ID token/Access Token
17 | UserAgent -> Client: Redireciona para o URI de redirecionamento com o código de autorização e o ID token/Access Token
18 | Client -> User: Redireciona para a URL de callback com o código de autorização e o ID token/Access Token
19 | User -> Client: Acessa a URL de callback
20 | Client -> AuthorizationServer: Troca o código de autorização por tokens de acesso e ID token
21 | AuthorizationServer -> Client: Retorna os tokens de acesso e ID token
22 | Client -> User: Exibe recurso protegido
23 |
24 | @enduml
--------------------------------------------------------------------------------
/authentication-flow/hybrid-flow/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
;
14 | }
15 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/src/PrivateRoute.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useContext } from "react";
2 | import { AuthContext } from "./AuthProvider";
3 | import { Navigate } from "react-router-dom";
4 |
5 | export function PrivateRoute(props: PropsWithChildren) {
6 | const authContext = useContext(AuthContext);
7 |
8 | if (!authContext.auth) {
9 | return ;
10 | }
11 |
12 | return props.children;
13 | }
14 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/src/utils.ts:
--------------------------------------------------------------------------------
1 | import Cookies from "js-cookie";
2 | import { decodeJwt } from "jose";
3 |
4 | export function makeLoginUrl() {
5 | const nonce = Math.random().toString(36);
6 | const state = Math.random().toString(36);
7 |
8 | //lembrar armazenar com cookie seguro (https)
9 | Cookies.set("nonce", nonce);
10 | Cookies.set("state", state);
11 |
12 | const loginUrlParams = new URLSearchParams({
13 | client_id: "fullcycle-client",
14 | redirect_uri: "http://localhost:3000/callback",
15 | response_type: "token id_token",
16 | nonce: nonce,
17 | state: state,
18 | });
19 |
20 | return `http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/auth?${loginUrlParams.toString()}`;
21 | }
22 |
23 | export function login(accessToken: string, idToken: string, state: string) {
24 | const stateCookie = Cookies.get("state");
25 | if (stateCookie !== state) {
26 | throw new Error("Invalid state");
27 | }
28 |
29 | let decodedAccessToken = null;
30 | let decodedIdToken = null;
31 | try {
32 | decodedAccessToken = decodeJwt(accessToken);
33 | decodedIdToken = decodeJwt(idToken);
34 | } catch (e) {
35 | throw new Error("Invalid token");
36 | }
37 |
38 | if (decodedAccessToken.nonce !== Cookies.get("nonce")) {
39 | throw new Error("Invalid nonce");
40 | }
41 |
42 | if (decodedIdToken.nonce !== Cookies.get("nonce")) {
43 | throw new Error("Invalid nonce");
44 | }
45 |
46 | Cookies.set("access_token", accessToken);
47 | Cookies.set("id_token", idToken);
48 |
49 | return decodedAccessToken;
50 | }
51 |
52 | export function getAuth() {
53 | const token = Cookies.get("access_token");
54 |
55 | if (!token) {
56 | return null;
57 | }
58 |
59 | try {
60 | return decodeJwt(token);
61 | } catch (e) {
62 | console.error(e);
63 | return null;
64 | }
65 | }
66 |
67 | export function makeLogoutUrl() {
68 | if (!Cookies.get("id_token")) {
69 | return false;
70 | }
71 | const logoutParams = new URLSearchParams({
72 | //client_id: "fullcycle-client",
73 | id_token_hint: Cookies.get("id_token") as string,
74 | post_logout_redirect_uri: "http://localhost:3000/login",
75 | });
76 |
77 | Cookies.remove("access_token");
78 | Cookies.remove("id_token");
79 | Cookies.remove("nonce");
80 | Cookies.remove("state");
81 |
82 | return `http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/logout?${logoutParams.toString()}`;
83 | }
84 |
85 | //http://localhost:3000/callback#error=unauthorized_client&error_description=Client+is+not+allowed+to+initiate+browser+login+with+given+response_type.+Implicit+flow+is+disabled+for+the+client.&state=0.qka67jgt2m
86 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | port: 3000,
9 | host: '0.0.0.0'
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/authentication-flow/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "authorization-code": "nodemon --exec ts-node-esm ./authorization-code-flow/src/index.ts",
8 | "implicit": "cd implicit-flow && npm run dev",
9 | "hybrid": "cd hybrid-flow && npm run dev",
10 | "resource-owner": "nodemon --exec ts-node-esm ./resource-owner-password-credentials/src/index.ts"
11 | },
12 | "workspaces": [
13 | "implicit-flow",
14 | "hybrid-flow"
15 | ],
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "devDependencies": {
20 | "@types/express": "^4.17.17",
21 | "@types/express-session": "^1.17.7",
22 | "@types/jsonwebtoken": "^9.0.2",
23 | "nodemon": "^2.0.22",
24 | "ts-node": "^10.9.1",
25 | "typescript": "^5.0.4"
26 | },
27 | "dependencies": {
28 | "express": "^4.18.2",
29 | "express-session": "^1.17.3",
30 | "jsonwebtoken": "^9.0.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/authentication-flow/resource-owner-password-credentials/README.md:
--------------------------------------------------------------------------------
1 | # Resource Owner Password Credentials ou Direct Grant
2 |
3 | O fluxo Resource Owner Password Credentials (ROPC) é um fluxo de autorização OAuth 2.0 que permite que um cliente obtenha um token de acesso usando diretamente as credenciais do usuário, como nome de usuário e senha. Embora esse fluxo possa ser conveniente em alguns casos, ele também apresenta algumas desvantagens e preocupações de segurança significativas em comparação com outros fluxos OAuth.
4 |
5 | Cenários úteis:
6 |
7 | * Implementação mais simples: O fluxo ROPC pode ser mais fácil de implementar, pois requer menos interações com o servidor de autorização. Ele é útil quando você tem controle total sobre o cliente e o servidor de autorização, o que significa que você pode garantir a segurança adequada.
8 |
9 | * Aplicativos confiáveis: Se você está desenvolvendo um aplicativo confiável e tem controle total sobre o cliente e o servidor de autorização, pode optar pelo fluxo ROPC. Por exemplo, se você está construindo um aplicativo móvel oficial para sua própria plataforma e pode garantir a segurança adequada do armazenamento de credenciais do usuário, o fluxo ROPC pode ser uma opção razoável.
10 |
11 | No entanto, é importante mencionar as desvantagens e preocupações de segurança associadas ao uso do fluxo ROPC:
12 |
13 | * Exposição direta das credenciais do usuário: Ao utilizar o fluxo ROPC, o cliente precisa solicitar e armazenar as credenciais de login do usuário. Isso introduz um risco de segurança, pois as credenciais podem ser comprometidas caso ocorra uma violação do cliente ou do servidor de autorização.
14 |
15 | * Falta de suporte para autenticação em dois fatores: O fluxo ROPC não oferece suporte nativo para autenticação em dois fatores. Como resultado, o uso desse fluxo pode levar à diminuição da segurança, uma vez que um fator adicional de autenticação não é considerado.
16 |
17 | * Restrições de escopo: O fluxo ROPC não oferece suporte a escopos granulares. Ao usar esse fluxo, o cliente solicita um token de acesso para todo o conjunto de recursos que o usuário possui acesso. Isso pode ser problemático se o cliente precisar de acesso a recursos específicos e não quiser solicitar acesso a todos eles.
18 |
19 | Em geral, embora o fluxo ROPC possa ser útil em certos cenários específicos, é recomendado usar fluxos de autorização mais seguros e robustos, como o fluxo Authorization Code ou o fluxo Implicit, que fornecem camadas adicionais de segurança e flexibilidade.
20 |
21 | ## Single Sign On
22 |
23 | O fluxo Resource Owner Password Credentials (ROPC) não é projetado para suportar Single Sign-On (SSO) de maneira nativa. O objetivo principal do fluxo ROPC é permitir que um cliente obtenha um token de acesso usando as credenciais do usuário diretamente.
24 |
25 | O Single Sign-On, por outro lado, é um conceito em que um usuário autenticado pode acessar vários sistemas ou aplicativos sem precisar fornecer credenciais adicionais. Ele geralmente envolve a utilização de um provedor de identidade centralizado que autentica o usuário e emite tokens de autenticação válidos para vários aplicativos.
26 |
27 | Para implementar o SSO, é mais comum usar fluxos de autorização baseados em tokens, como o fluxo Authorization Code ou o fluxo Implicit, juntamente com um provedor de identidade, como o OpenID Connect (OIDC). Esses fluxos e protocolos são projetados para suportar cenários de autenticação federada, em que o provedor de identidade pode autenticar o usuário e fornecer tokens de acesso que são válidos em vários aplicativos.
28 |
29 | Portanto, se você está procurando uma solução de Single Sign-On, é recomendado explorar as opções disponíveis em protocolos como OAuth 2.0 e OpenID Connect, em vez de usar o fluxo ROPC. Essas tecnologias oferecem recursos e recursos mais adequados para suportar cenários de autenticação federada e SSO.
30 |
31 |
--------------------------------------------------------------------------------
/authentication-flow/resource-owner-password-credentials/resource.owner-password-credentials.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devfullcycle/fc-keycloak/006da5f1d9bedacfbcfe88143789b82ea28e0546/authentication-flow/resource-owner-password-credentials/resource.owner-password-credentials.png
--------------------------------------------------------------------------------
/authentication-flow/resource-owner-password-credentials/resource.owner-password-credentials.wsd:
--------------------------------------------------------------------------------
1 | @startuml
2 | actor User
3 | participant "Client Application" as Client
4 | participant "Authorization Server" as AuthServer
5 | participant "Resource Server" as ResourceServer
6 |
7 | User -> Client: Provide username and password
8 | Client -> AuthServer: Send token request with credentials
9 | AuthServer -> AuthServer: Authenticate credentials
10 | AuthServer -> Client: Return access token
11 | Client -> ResourceServer: Send API request with access token
12 | ResourceServer -> ResourceServer: Validate access token
13 | ResourceServer -> Client: Return requested resource
14 | @enduml
--------------------------------------------------------------------------------
/authentication-flow/resource-owner-password-credentials/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import session from "express-session";
3 |
4 | const app = express();
5 | app.use(express.urlencoded({ extended: true }));
6 |
7 | const memoryStore = new session.MemoryStore();
8 |
9 | app.use(
10 | session({
11 | secret: "my-secret",
12 | resave: false,
13 | saveUninitialized: false,
14 | store: memoryStore,
15 | //expires
16 | })
17 | );
18 |
19 | const middlewareIsAuth = (
20 | req: express.Request,
21 | res: express.Response,
22 | next: express.NextFunction
23 | ) => {
24 | //@ts-expect-error - type mismatch
25 | if (!req.session.user) {
26 | return res.redirect("/login");
27 | }
28 | next();
29 | };
30 |
31 | app.get("/login", (req, res) => {
32 | //@ts-expect-error - type mismatch
33 | if (req.session.user) {
34 | return res.redirect("/admin");
35 | }
36 | res.sendFile(__dirname + "/login.html");
37 | });
38 |
39 | app.post("/login", async (req, res) => {
40 | const { username, password } = req.body;
41 |
42 | const response = await fetch(
43 | "http://host.docker.internal:8080/realms/fullcycle-realm/protocol/openid-connect/token",
44 | {
45 | method: "POST",
46 | headers: {
47 | "Content-Type": "application/x-www-form-urlencoded",
48 | },
49 | body: new URLSearchParams({
50 | client_id: "fullcycle-client",
51 | grant_type: "password",
52 | username,
53 | password,
54 | scope: "openid",
55 | }).toString(),
56 | }
57 | );
58 |
59 | const result = await response.json();
60 | console.log(result);
61 | //@ts-expect-error - type mismatch
62 | req.session.user = result;
63 | req.session.save();
64 |
65 | res.redirect("/admin");
66 | });
67 |
68 | app.get("/logout", async (req, res) => {
69 | // const logoutParams = new URLSearchParams({
70 | // //client_id: "fullcycle-client",
71 | // //@ts-expect-error
72 | // id_token_hint: req.session.user.id_token,
73 | // post_logout_redirect_uri: "http://localhost:3000/login",
74 | // });
75 |
76 | // req.session.destroy((err) => {
77 | // console.error(err);
78 | // });
79 |
80 | // const url = `http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/logout?${logoutParams.toString()}`;
81 | await fetch(
82 | "http://host.docker.internal:8080/realms/fullcycle-realm/protocol/openid-connect/revoke",
83 | {
84 | method: "POST",
85 | headers: {
86 | "Content-Type": "application/x-www-form-urlencoded",
87 | },
88 | body: new URLSearchParams({
89 | client_id: "fullcycle-client",
90 | //@ts-expect-error
91 | token: req.session.user.refresh_token,
92 | }).toString(),
93 | }
94 | );
95 | //response.ok verificar se a resposta está ok
96 | req.session.destroy((err) => {
97 | console.error(err);
98 | });
99 | res.redirect("/login");
100 | });
101 |
102 | app.get("/admin", middlewareIsAuth, (req, res) => {
103 | //@ts-expect-error - type mismatch
104 | res.json(req.session.user);
105 | });
106 |
107 | app.listen(3000, () => {
108 | console.log("Listening on port 3000");
109 | });
110 |
--------------------------------------------------------------------------------
/authentication-flow/resource-owner-password-credentials/src/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | OAuth 2.0 Resource Owner Password Credentials Flow Example
4 |
5 |
6 |
OAuth 2.0 Resource Owner Password Credentials Flow Example
7 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/authentication-flow/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42 | // "resolveJsonModule": true, /* Enable importing .json files. */
43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
45 |
46 | /* JavaScript Support */
47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
50 |
51 | /* Emit */
52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
58 | // "outDir": "./", /* Specify an output folder for all emitted files. */
59 | // "removeComments": true, /* Disable emitting comments. */
60 | // "noEmit": true, /* Disable emitting files from a compilation. */
61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
68 | // "newLine": "crlf", /* Set the newline character for emitting files. */
69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
75 |
76 | /* Interop Constraints */
77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
83 |
84 | /* Type Checking */
85 | "strict": true, /* Enable all strict type-checking options. */
86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
104 |
105 | /* Completeness */
106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/keycloak/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | keycloak:
6 | image: quay.io/keycloak/keycloak:21.1
7 | command: start-dev
8 | ports:
9 | - 8080:8080
10 | environment:
11 | - KEYCLOAK_ADMIN=admin
12 | - KEYCLOAK_ADMIN_PASSWORD=admin
13 | - KC_DB=mysql
14 | - KC_DB_URL=jdbc:mysql://db:3306/keycloak
15 | - KC_DB_USERNAME=root
16 | - KC_DB_PASSWORD=root
17 | depends_on:
18 | db:
19 | condition: service_healthy
20 |
21 | db:
22 | image: mysql:8.0.30-debian
23 | volumes:
24 | - ./.docker/dbdata:/var/lib/mysql
25 | environment:
26 | - MYSQL_ROOT_PASSWORD=root
27 | - MYSQL_DATABASE=keycloak
28 | security_opt:
29 | - seccomp:unconfined
30 | healthcheck:
31 | test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
32 | interval: 5s
33 | timeout: 10s
34 | retries: 3
--------------------------------------------------------------------------------