├── .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 | ![Authorization Code Flow](./authentication-code.png) 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 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hybrid-flow", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "jose": "^4.14.4", 14 | "js-cookie": "^3.0.5", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.12.0" 18 | }, 19 | "devDependencies": { 20 | "@types/js-cookie": "^3.0.3", 21 | "@types/react": "^18.0.37", 22 | "@types/react-dom": "^18.0.11", 23 | "@typescript-eslint/eslint-plugin": "^5.59.0", 24 | "@typescript-eslint/parser": "^5.59.0", 25 | "@vitejs/plugin-react": "^4.0.0", 26 | "eslint": "^8.38.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.3.4", 29 | "typescript": "^5.0.2", 30 | "vite": "^4.3.9" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/src/Admin.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { AuthContext } from "./AuthProvider"; 3 | 4 | export function Admin() { 5 | const { auth } = useContext(AuthContext); 6 | 7 | return ( 8 |
9 |

Admin

10 |
{JSON.stringify(auth)}
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider, createBrowserRouter } from "react-router-dom"; 2 | import { Login } from "./Login"; 3 | import { Logout } from "./Logout"; 4 | import { Callback } from "./Callback"; 5 | import { AuthProvider } from "./AuthProvider"; 6 | import { Admin } from "./Admin"; 7 | import { PrivateRoute } from "./PrivateRoute"; 8 | 9 | const router = createBrowserRouter([ 10 | { 11 | path: "login", 12 | element: , 13 | }, 14 | { 15 | path: "logout", 16 | element: , 17 | }, 18 | { 19 | path: "admin", 20 | element: ( 21 | 22 | 23 | 24 | ), 25 | }, 26 | { 27 | path: "callback", 28 | element: , 29 | }, 30 | ]); 31 | 32 | function App() { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/src/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, createContext, useCallback, useState } from "react"; 2 | import * as utils from "./utils"; 3 | import { JWTPayload } from "jose"; 4 | 5 | type AuthContextProps = { 6 | auth: JWTPayload | null; 7 | makeLoginUrl: () => string; 8 | makeLogoutUrl: () => string; 9 | login: ( 10 | accessToken: string, 11 | idToken: string, 12 | code: string, 13 | state: string 14 | ) => JWTPayload; 15 | }; 16 | 17 | const initContextData: AuthContextProps = { 18 | auth: null, 19 | makeLoginUrl: utils.makeLoginUrl, 20 | //@ts-expect-error - this is a mock function 21 | // eslint-disable-next-line @typescript-eslint/no-empty-function 22 | makeLogoutUrl: () => {}, 23 | //@ts-expect-error - this is a mock function 24 | // eslint-disable-next-line @typescript-eslint/no-empty-function 25 | login: () => {}, 26 | }; 27 | 28 | //create a context for the login state 29 | export const AuthContext = createContext(initContextData); 30 | 31 | //create a provider for the login state 32 | export const AuthProvider = (props: PropsWithChildren) => { 33 | const makeLogin = useCallback( 34 | (accessToken: string, idToken: string, code: string, state: string) => { 35 | //@ts-expect-error - for refresh token param 36 | const authData = utils.login(accessToken, idToken, null, state); 37 | setData((oldData) => ({ 38 | auth: authData, 39 | makeLoginUrl: oldData.makeLoginUrl, 40 | makeLogoutUrl: oldData.makeLogoutUrl, 41 | login: oldData.login, 42 | })); 43 | utils.exchangeCodeForToken(code).then((authData) => { 44 | setData((oldData) => ({ 45 | auth: authData, 46 | makeLoginUrl: oldData.makeLoginUrl, 47 | makeLogoutUrl: oldData.makeLogoutUrl, 48 | login: oldData.login, 49 | })); 50 | }); 51 | return authData; 52 | }, 53 | [] 54 | ); 55 | 56 | const [data, setData] = useState({ 57 | auth: utils.getAuth(), 58 | makeLoginUrl: utils.makeLoginUrl, 59 | makeLogoutUrl: utils.makeLogoutUrl, 60 | login: makeLogin, 61 | }); 62 | 63 | return ( 64 | {props.children} 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/src/Callback.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import { AuthContext } from "./AuthProvider"; 4 | 5 | export function Callback() { 6 | const { hash } = useLocation(); 7 | const { login, auth } = useContext(AuthContext); 8 | const navigate = useNavigate(); 9 | 10 | useEffect(() => { 11 | if (auth) { 12 | navigate("/login"); 13 | return; 14 | } 15 | 16 | const searchParams = new URLSearchParams(hash.replace("#", "")); 17 | const accessToken = searchParams.get("access_token") as string; 18 | const idToken = searchParams.get("id_token") as string; 19 | const state = searchParams.get("state") as string; 20 | const code = searchParams.get("code") as string; 21 | 22 | if (!accessToken || !idToken || !state) { 23 | navigate("/login"); 24 | } 25 | 26 | login(accessToken, idToken, code, state); 27 | 28 | }, [hash, login, auth, navigate]); 29 | 30 | return
Loading...
; 31 | } 32 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/src/Login.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | import { makeLoginUrl } from "./utils"; 4 | import { AuthContext } from "./AuthProvider"; 5 | 6 | export function Login() { 7 | const { auth } = useContext(AuthContext); 8 | 9 | useEffect(() => { 10 | if (!auth) { 11 | window.location.href = makeLoginUrl(); 12 | } 13 | }, [auth]); 14 | 15 | return auth ? :
Loading...
; 16 | } 17 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/src/Logout.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { AuthContext } from "./AuthProvider"; 3 | 4 | export function Logout() { 5 | const { makeLogoutUrl } = useContext(AuthContext); 6 | useEffect(() => { 7 | const logoutUrl = makeLogoutUrl(); 8 | if (logoutUrl) { 9 | window.location.href = logoutUrl; 10 | } 11 | }, [makeLogoutUrl]); 12 | 13 | return
Loading...
; 14 | } 15 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-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/hybrid-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/hybrid-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 code", 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 exchangeCodeForToken(code: string) { 24 | const tokenUrlParams = new URLSearchParams({ 25 | client_id: "fullcycle-client", 26 | grant_type: "authorization_code", 27 | code: code, 28 | redirect_uri: "http://localhost:3000/callback", 29 | nonce: Cookies.get("nonce") as string, 30 | }); 31 | 32 | return fetch( 33 | "http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/token", 34 | { 35 | method: "POST", 36 | headers: { 37 | "Content-Type": "application/x-www-form-urlencoded", 38 | }, 39 | body: tokenUrlParams.toString(), 40 | } 41 | ) 42 | .then((res) => res.json()) 43 | .then((res) => { 44 | return login(res.access_token, null, res.refresh_token); 45 | }); 46 | } 47 | 48 | export function login( 49 | accessToken: string, 50 | idToken: string | null, 51 | refreshToken?: string, 52 | state?: string 53 | ) { 54 | const stateCookie = Cookies.get("state"); 55 | if (state && stateCookie !== state) { 56 | throw new Error("Invalid state"); 57 | } 58 | 59 | let decodedAccessToken = null; 60 | let decodedIdToken = null; 61 | let decodedRefreshToken = null; 62 | try { 63 | decodedAccessToken = decodeJwt(accessToken); 64 | 65 | if (idToken) { 66 | decodedIdToken = decodeJwt(idToken); 67 | } 68 | 69 | if (refreshToken) { 70 | decodedRefreshToken = decodeJwt(refreshToken); 71 | } 72 | } catch (e) { 73 | console.error(e); 74 | throw new Error("Invalid token"); 75 | } 76 | 77 | if (decodedAccessToken.nonce !== Cookies.get("nonce")) { 78 | throw new Error("Invalid nonce"); 79 | } 80 | 81 | if (decodedIdToken && decodedIdToken.nonce !== Cookies.get("nonce")) { 82 | throw new Error("Invalid nonce"); 83 | } 84 | 85 | if ( 86 | decodedRefreshToken && 87 | decodedRefreshToken.nonce !== Cookies.get("nonce") 88 | ) { 89 | throw new Error("Invalid nonce"); 90 | } 91 | 92 | Cookies.set("access_token", accessToken); 93 | if (idToken){ 94 | Cookies.set("id_token", idToken); 95 | } 96 | if (decodedRefreshToken) { 97 | Cookies.set("refresh_token", refreshToken as string); 98 | } 99 | 100 | return decodedAccessToken; 101 | } 102 | 103 | export function getAuth() { 104 | const token = Cookies.get("access_token"); 105 | 106 | if (!token) { 107 | return null; 108 | } 109 | 110 | try { 111 | return decodeJwt(token); 112 | } catch (e) { 113 | console.error(e); 114 | return null; 115 | } 116 | } 117 | 118 | export function makeLogoutUrl() { 119 | if (!Cookies.get("id_token")) { 120 | return false; 121 | } 122 | const logoutParams = new URLSearchParams({ 123 | //client_id: "fullcycle-client", 124 | id_token_hint: Cookies.get("id_token") as string, 125 | post_logout_redirect_uri: "http://localhost:3000/login", 126 | }); 127 | 128 | Cookies.remove("access_token"); 129 | Cookies.remove("id_token"); 130 | Cookies.remove("refresh_token"); 131 | Cookies.remove("nonce"); 132 | Cookies.remove("state"); 133 | 134 | return `http://localhost:8080/realms/fullcycle-realm/protocol/openid-connect/logout?${logoutParams.toString()}`; 135 | } 136 | 137 | //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 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-flow/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /authentication-flow/hybrid-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/hybrid-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/hybrid-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/implicit-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/implicit-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/implicit-flow/README.md: -------------------------------------------------------------------------------- 1 | # Implicit Flow 2 | 3 | O fluxo Implicit Flow é geralmente recomendado para aplicativos de página única (single-page applications) e aplicativos móveis em que a confidencialidade do cliente não é uma prioridade. Esse fluxo simplifica o processo de autorização, permitindo que o cliente (aplicativo) obtenha o token de acesso diretamente do servidor de autorização, sem a necessidade de uma troca adicional de código de autorização. 4 | 5 | O fluxo Implicit Flow é mais adequado quando você possui um aplicativo front-end JavaScript que interage diretamente com o servidor de autorização. Nesse fluxo, o aplicativo faz uma solicitação de autorização ao servidor de autorização, que responde fornecendo o token de acesso diretamente no navegador do usuário. 6 | 7 | No entanto, o fluxo Implicit Flow apresenta algumas limitações de segurança em comparação com o fluxo Authorization Code Flow. O token de acesso é retornado diretamente no navegador do usuário, o que pode expô-lo a ataques de cross-site scripting (XSS) se as devidas precauções de segurança não forem tomadas. Além disso, o fluxo Implicit Flow não permite a renovação de tokens de acesso sem a intervenção do usuário. 8 | 9 | Em resumo, o fluxo Implicit Flow é recomendado para aplicativos de página única e aplicativos móveis em que a confidencialidade do cliente não é uma preocupação crítica, e quando a simplicidade de obtenção do token de acesso diretamente no navegador do usuário é uma consideração importante. 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. 10 | 11 | Uma coisa a observar é que apenas um token de acesso é fornecido e não há token de atualização. Isso significa que uma vez que o token de acesso expirou, o aplicativo deve fazer o redirecionamento para o Keycloak novamente para obter um novo token de acesso. 12 | 13 | ## Single Sign On 14 | 15 | Similar ao Authorization Code Flow, o Implicit 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 | ## Configuração 18 | 19 | response_type=token ou response_type=token id_token 20 | 21 | ## A hashtag é acrescentada ao redirecionamento 22 | 23 | http://localhost:3000/callback#state=YKWruxpK9QzU3vwOi5IxWg%3D%3D&session_state=593006ef-0a13-4472-bfef-5f4776fe3441&access_token=XXXXX&id_token=XXXXX&token_type=Bearer&expires_in=900 -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/implicit-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfullcycle/fc-keycloak/006da5f1d9bedacfbcfe88143789b82ea28e0546/authentication-flow/implicit-flow/implicit-flow.png -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/implicit-flow.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | actor User 3 | participant Client 4 | participant AuthorizationServer 5 | participant ResourceServer 6 | 7 | User -> Client: Acessa o aplicativo 8 | Client -> AuthorizationServer: Solicita autorização 9 | AuthorizationServer -> User: Exibe tela de login 10 | User -> AuthorizationServer: Insere credenciais 11 | AuthorizationServer -> User: Autentica o usuário 12 | User -> AuthorizationServer: Autoriza o acesso 13 | AuthorizationServer -> Client: Redireciona para o URI de redirecionamento com o token de acesso/id token 14 | Client -> User: Exibe recurso protegido 15 | @enduml 16 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "implicit-flow", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "jose": "^4.14.4", 14 | "js-cookie": "^3.0.5", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.12.0" 18 | }, 19 | "devDependencies": { 20 | "@types/js-cookie": "^3.0.3", 21 | "@types/react": "^18.0.37", 22 | "@types/react-dom": "^18.0.11", 23 | "@typescript-eslint/eslint-plugin": "^5.59.0", 24 | "@typescript-eslint/parser": "^5.59.0", 25 | "@vitejs/plugin-react": "^4.0.0", 26 | "eslint": "^8.38.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.3.4", 29 | "typescript": "^5.0.2", 30 | "vite": "^4.3.9" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/src/Admin.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { AuthContext } from "./AuthProvider"; 3 | 4 | export function Admin() { 5 | const { auth } = useContext(AuthContext); 6 | 7 | return ( 8 |
9 |

Admin

10 |
{JSON.stringify(auth)}
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider, createBrowserRouter } from "react-router-dom"; 2 | import { Login } from "./Login"; 3 | import { Logout } from "./Logout"; 4 | import { Callback } from "./Callback"; 5 | import { AuthProvider } from "./AuthProvider"; 6 | import { Admin } from "./Admin"; 7 | import { PrivateRoute } from "./PrivateRoute"; 8 | 9 | const router = createBrowserRouter([ 10 | { 11 | path: "login", 12 | element: , 13 | }, 14 | { 15 | path: "logout", 16 | element: , 17 | }, 18 | { 19 | path: "admin", 20 | element: ( 21 | 22 | 23 | 24 | ), 25 | }, 26 | { 27 | path: "callback", 28 | element: , 29 | }, 30 | ]); 31 | 32 | function App() { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/src/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, createContext, useCallback, useState } from "react"; 2 | import * as utils from "./utils"; 3 | import { JWTPayload } from "jose"; 4 | 5 | type AuthContextProps = { 6 | auth: JWTPayload | null; 7 | makeLoginUrl: () => string; 8 | makeLogoutUrl: () => string; 9 | login: (accessToken: string, idToken: string, state: string) => JWTPayload; 10 | }; 11 | 12 | const initContextData: AuthContextProps = { 13 | auth: null, 14 | makeLoginUrl: utils.makeLoginUrl, 15 | //@ts-expect-error - this is a mock function 16 | // eslint-disable-next-line @typescript-eslint/no-empty-function 17 | makeLogoutUrl: () => {}, 18 | //@ts-expect-error - this is a mock function 19 | // eslint-disable-next-line @typescript-eslint/no-empty-function 20 | login: () => {}, 21 | }; 22 | 23 | //create a context for the login state 24 | export const AuthContext = createContext(initContextData); 25 | 26 | //create a provider for the login state 27 | export const AuthProvider = (props: PropsWithChildren) => { 28 | const makeLogin = useCallback( 29 | (accessToken: string, idToken: string, state: string) => { 30 | const authData = utils.login(accessToken, idToken, state); 31 | setData((oldData) => ({ 32 | auth: authData, 33 | makeLoginUrl: oldData.makeLoginUrl, 34 | makeLogoutUrl: oldData.makeLogoutUrl, 35 | login: oldData.login, 36 | })); 37 | return authData; 38 | }, 39 | [] 40 | ); 41 | 42 | const [data, setData] = useState({ 43 | auth: utils.getAuth(), 44 | makeLoginUrl: utils.makeLoginUrl, 45 | makeLogoutUrl: utils.makeLogoutUrl, 46 | login: makeLogin, 47 | }); 48 | 49 | return ( 50 | {props.children} 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/src/Callback.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import { AuthContext } from "./AuthProvider"; 4 | 5 | export function Callback() { 6 | const { hash } = useLocation(); 7 | const { login, auth } = useContext(AuthContext); 8 | const navigate = useNavigate(); 9 | 10 | useEffect(() => { 11 | if (auth) { 12 | navigate("/login"); 13 | return; 14 | } 15 | 16 | const searchParams = new URLSearchParams(hash.replace("#", "")); 17 | const accessToken = searchParams.get("access_token") as string; 18 | const idToken = searchParams.get("id_token") as string; 19 | const state = searchParams.get("state") as string; 20 | 21 | if (!accessToken || !idToken || !state) { 22 | navigate("/login"); 23 | } 24 | 25 | login(accessToken, idToken, state); 26 | 27 | }, [hash, login, auth, navigate]); 28 | 29 | return
Loading...
; 30 | } 31 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/src/Login.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | import { makeLoginUrl } from "./utils"; 4 | import { AuthContext } from "./AuthProvider"; 5 | 6 | export function Login() { 7 | const { auth } = useContext(AuthContext); 8 | 9 | useEffect(() => { 10 | if (!auth) { 11 | window.location.href = makeLoginUrl(); 12 | } 13 | }, [auth]); 14 | 15 | return auth ? :
Loading...
; 16 | } 17 | -------------------------------------------------------------------------------- /authentication-flow/implicit-flow/src/Logout.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { AuthContext } from "./AuthProvider"; 3 | 4 | export function Logout() { 5 | const { makeLogoutUrl } = useContext(AuthContext); 6 | useEffect(() => { 7 | const logoutUrl = makeLogoutUrl(); 8 | if (logoutUrl) { 9 | window.location.href = logoutUrl; 10 | } 11 | }, [makeLogoutUrl]); 12 | 13 | return
Loading...
; 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 |
8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 |
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 --------------------------------------------------------------------------------