将 OIDC 添加到具有受限路由的 React 应用程序

Adding OIDC to an React application with restricted routes

我想将 OIDC 添加到我的 React 应用程序中,我正在使用 oidc-client-ts 因为它看起来很受欢迎并且仍在维护中。我的问题是我错过了一些 React 示例。

我想要的只是保护一条线路。如果用户未通过身份验证,则应将他们重定向到登录屏幕,该屏幕具有使用自定义提供程序激活身份验证流程的按钮。

我尝试使用 these two 示例,但我不确定如何将它们粘合在一起以及如何将 Angular 代码转换为 React。

到目前为止,我已经将整个应用程序包装在一个 AuthContext 中,并将除一条路由之外的所有路由设为私有,如 first example:

index.tsx:

<StrictMode>
    <AuthProvider>
        <BrowserRouter>
            <Routes>
                <Route path={routes.LOGIN} element={<LoginContainer />} />
                <Route element={<Layout />}>
                    <Route index element={<Home />} />
                    <Route path="/openid/callback" element={<AuthCallback />} />
                        // Other pages
                </Route>
                <Route path="*" element={<ErrorPage />} />
            </Routes>
        </BrowserRouter>
    </AuthProvider>
</StrictMode>

具有专用路由的 Layout-组件,使除 "/login" 以外的所有路径都是专用的:

function RequireAuth({ children }: { children: JSX.Element }) {
    const auth = useAuth();

    if (!auth.user) {
        return <Navigate to="/login" replace />;
    }

    return children;
}

function Layout() {
    return (
        <RequireAuth>
            <>
                <Header />
                <Main />
                <Footer />
            </>
        </RequireAuth>
    );
}

AuthProvider:

const AuthContext = createContext<AuthContextType>(null!);

const useAuth = () => useContext(AuthContext);

function AuthProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState<any>(null);
    const authService = new AuthService();

    const login = () => authService.login().then(user1 => setUser(user1));

    const loginCallback = async () => {
        const authedUser = await authService.loginCallback();
        setUser(authedUser);
    };

    const logout = () => authService.login().then(() => setUser(null));
    const value = { user, login, logout };

    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export { AuthProvider, useAuth };

authService 只是从 angular example:

复制而来
import { User, UserManager } from "oidc-client-ts";

export default class AuthService {
    userManager: UserManager;

    constructor() {
        const settings = {
            authority: "...",
            client_id: "...",
            redirect_uri: "http://localhost:3000/openid/callback",
            client_secret: "...",
            post_logout_redirect_uri: "http://localhost:3000/login"
        };
        this.userManager = new UserManager(settings);
    }

    public getUser(): Promise<User | null> {
        return this.userManager.getUser();
    }

    public login(): Promise<void> {
        return this.userManager.signinRedirect();
    }

    public loginCallback(): Promise<User> {
        return this.userManager.signinRedirectCallback();
    }

    public logout(): Promise<void> {
        return this.userManager.signoutRedirect();
    }
}

我的问题是我不知道如何在 AuthProvider 中设置用户,因此我可以检查我是否在 RequireAuth 组件中获得授权。它没有在 thenAuthProvider 登录和注销功能中设置,所以每当我尝试登录时,我都会被重定向到登录页面。

有人可以告诉我如何使用 OIDC 进行身份验证流程并将我的所有路径限制为只允许经过身份验证的用户使用吗?

此外 这个答案 说应该有一个 AuthorizationCallback 组件来解析 URL。当我使用似乎为我解析数据的 oidc-client-ts 时,我真的需要这个额外的步骤还是我可以将重定向 URL 设为“/”或“/home”?

编辑:

我发现 signinRedirect 转到新的 URL,这意味着脚本的其余部分永远不会 运行。 signinRedirectCallback 是 returns 用户的调用。当我弄清楚如何正确保护路由后,我将 post 它作为答案。 RequireAuth 中的检查在设置用户之前完成。我如何 post 在设置用户之前进行检查,这样即使我已登录也不会重定向到登录?如果我刷新页面,我将失去 AuthProvideruser 状态,即使存在活动会话,我也会被发送到登录页面。当我以干净的方式加载应用程序时,我不确定我在哪里以及如何检查我是否有会话 运行ning。

问题

login 实用程序仅 return 是一个已解析的 Promise<void>,没有用户值,因此当您 可以 从 returned Promise 链中没有解析值 returned。

public login(): Promise<void> {
  return this.userManager.signinRedirect();
}

解决方案

成功验证后,您似乎必须调用 getUser 来获取当前用户值,一个 User 对象或 null。

public getUser(): Promise<User | null> {
  return this.userManager.getUser();
}

这是一个 login 在登录后获取用户的实现。

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<any>(null);
  const authService = new AuthService();

  const login = async () => {
    try {
      await authService.login();
      const user = await authService.getUser();
      setUser(user);
    } catch(error) {
      // log error, set error state, ignore, etc...
      setUser(null);
    }
  };

  const logout = () => {
    try {
      await authService.logout();
    } catch(error) {
      // log error, set error state, ignore, etc...
    } finally {
      setUser(null);
    }
  };

  const value = { user, login, logout };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

当然,您可以直接将 AuthService 更新为 return 经过身份验证的用户。

export default class AuthService {
  ...

  public login(): Promise<User | null> {
    return this.userManager.signinRedirect()
      .then(getUser)
      .catch(error => {
        // handle any errors, rejected Promises, etc...
        return null;
      });
  }

  // or

  public async login(): Promise<User | null> {
    try {
      await this.userManager.signinRedirect();
      return getUser();
    } catch(error) {
      // handle any errors, rejected Promises, etc...
      return null;
    }
  }

  ...
}

现在,原来的 AuthContext login 功能将按预期运行。

const login = () => authService.login()
  .then(user1 => setUser(user1))
  .catch((error) => {
    // log error, set error state, ignore, etc...
    setUser(null);
  });

  const login = async () => {
    try {
      const user = await authService.login();
      setUser(user);
    } catch(error) {
      // log error, set error state, ignore, etc...
      setUser(null);
    }
  };

更新

The check in RequireAuth is done before the user is set. How do I postpone the check until the user has been set so I do not redirect to login even though I am signed in?

为此,您可以使用一种“加载”状态。我通常建议要么添加显式加载状态,要么使用初始 auth 状态值,该值既不匹配经过身份验证的状态,也不匹配未经过身份验证的状态。

例如,如果 null 是未经身份验证的用户,而某些用户对象是经过身份验证的用户,则使用 undefined 初始值。

授权上下文

const [user, setUser] = useState<any>();

...

function RequireAuth({ children }: { children: JSX.Element }) {
  const auth = useAuth();

  if (auth === undefined) {
    return null; // or loading spinner, etc...
  }

  return auth.user ? children : <Navigate to="/login" replace />;
}

And if I refresh the page I lose the user state from AuthProvider and I will be sent to the login page even though there is an active session.

这是因为重新加载页面时,整个应用程序都会重新加载。任何本地组件状态都会丢失。你可以试试initializing/persisting状态到localStorage。

示例:

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<any>(
    JSON.parse(localStorage.getItem('user')) || undefined
  );

  useEffect(() => {
    localStorage.setItem('user', JSON.stringify(user));
  }, [user]);

  ...
}

我从 Drew Reese 的回答中得到启发,设法解决了这个问题。 问题是 oidc-client-ts' signinRedirect will redirect to the authentication server and thus the React code will stop it's execution and the then block is never run. The trick is to use signinRedirectCallback 是 login-call 之后的 processes the response from the authorization endpoint。因此,当我点击重定向 url (http://localhost:3000/openid/callback) 时,我调用 signinRedirectCallback 来确定我是否应该转到 HomeLogin-组件。所以除了 /login 和 post-login-redirect-url 之外的所有路线都将是 auth-protected:

<Routes>
  <Route path={routes.LOGIN} element={<LoginContainer />} />
  <Route path="/openid/callback" element={<AuthCallback />} />
  <Route element={<LayoutWithAuth />}>
    <Route index element={<Home />} />
    ...
  </Route>
  <Route path="*" element={<ErrorPage />} />
</Routes>;

然后在重定向回应用程序时,loginCallback/signingRedirectCallback(第一个只是将呼叫转发给 authService),在 AuthContext 中设置用户(见下文) 然后我导航到主页:

AuthCallback:

function AuthCallback() {
  const auth = useAuth();
  const navigate = useNavigate();

  useEffect(() => {
    auth.loginCallback().then(() => {
      navigate(routes.INDEX);
    });
  }, []);

  return <div>Processing signin...</div>;
}

通过使 loginCallback 异步,我确保当我重定向到登录页面时,用户将在 LayoutWithAuth-组件执行 auth-check 时设置:

function RequireAuth({ children }: { children: JSX.Element }) {
  const auth = useAuth();
  return auth.user === undefined ? <Navigate to="/login" replace /> : children;
}

function LayoutWithAuth() {
  return (
    <RequireAuth>
      <>
        <Header />
        <Body />
        <Footer />
      </>
    </RequireAuth>
  );
}

oidc-client-ts 将用户保存在 sessionStorage 中,因此如果页面刷新,AuthContext 将首先检查 sessionStorage 以查看用户是否已授权:

AuthContext:

const AuthContext = createContext<AuthContextType>(null!);

const useAuth = () => useContext(AuthContext);

function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null | undefined>(
    JSON.parse(
      sessionStorage.getItem(process.env.REACT_APP_SESSION_ID!) || "null"
    ) || undefined
  );

  const authService = new AuthService();

  const loginCallback = async (): Promise<void> => {
    const authedUser = await authService.loginCallback();
    setUser(authedUser);
  };

  // Login and logout methods

  const value = { user, login, logout };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export { AuthProvider, useAuth };