;
31 | }
32 |
--------------------------------------------------------------------------------
/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/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/authentication-flow/implicit-flow/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------