Skip to content

Instantly share code, notes, and snippets.

@ulrikstrid
Last active April 12, 2021 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ulrikstrid/d0a79572f9bb73af6cd33cdf9764f680 to your computer and use it in GitHub Desktop.
Save ulrikstrid/d0a79572f9bb73af6cd33cdf9764f680 to your computer and use it in GitHub Desktop.
Msal Bindings and react component
type status =
| Unauthenticated
| Authenticating
| Authenticated
| AuthenticationError(string);
type state = {
status,
account: option(MsalBrowser.Account.t),
id_token: option(MsalBrowser.IdToken.t),
id_token_string: option(string),
access_token: option(string),
userState: option(string),
};
type action =
| StartAuth
| ClearAuth
| SetAccount(MsalBrowser.Account.t)
| SetResponse(MsalBrowser.authResponse)
| SetError(string);
[@react.component]
let make =
(
~clientId,
~scopes,
~authority="https://login.microsoftonline.com/common",
~redirectUri,
~userState=?,
~loadingComponent=?,
~extraQueryParameters=?,
~prompt="select_account",
~children,
) => {
let (agentApp, setAgentApp) =
React.useState(() => {
let agentConfig: MsalBrowser.PublicClientApplication.config = {
auth: {
clientId,
redirectUri,
authority,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
},
};
MsalBrowser.makePublicClientApplication(~config=agentConfig);
});
let (logout, setLogout) =
React.useState(() => MsalBrowser.logout(agentApp));
React.useEffect3(
() => {
let agentConfig: MsalBrowser.PublicClientApplication.config = {
auth: {
clientId,
redirectUri,
authority,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
},
};
let agentApp =
MsalBrowser.makePublicClientApplication(~config=agentConfig);
setLogout(_ => MsalBrowser.logout(agentApp));
setAgentApp(_ => agentApp);
None;
},
(clientId, redirectUri, authority),
);
let (state, send) =
React.useReducer(
(state, action) => {
switch (action) {
| StartAuth => {...state, status: Authenticating}
| SetAccount(account) => {
...state,
account: Some(account),
status: Authenticated,
}
| SetResponse(response) => {
id_token: Some(response.idTokenClaims),
id_token_string: Some(response.idToken),
access_token: Some(response.accessToken),
userState: Some(response.accountState),
account: Some(response.account),
status: Authenticated,
}
| ClearAuth => {...state, status: Unauthenticated}
| SetError(message) => {
...state,
status: AuthenticationError(message),
}
}
},
{
status: Unauthenticated,
id_token: None,
id_token_string: None,
account: None,
access_token: None,
userState,
},
);
React.useEffect1(
() => {
let authCallback = (response: Js.Nullable.t(MsalBrowser.authResponse)) => {
switch (response->Js.Nullable.toOption) {
| Some(response) =>
send(SetResponse(response));
Js.Promise.resolve();
| None =>
MsalBrowser.getAllAccounts(agentApp)->Belt.Array.get(0)
|> (
fun
| Some(account) => send(SetAccount(account))
| None => ()
);
Js.Promise.resolve();
};
};
let _ =
MsalBrowser.handleRedirectPromise(agentApp)
|> Js.Promise.then_(authCallback)
|> Js.Promise.catch(e => {
[%log.error "AAD Auth redirect"; ("error", e)];
send(SetError(Obj.magic(e)));
Js.Promise.resolve();
});
None;
},
[||],
);
React.useEffect3(
() => {
if (Belt.Option.isSome(state.account)) {
if (state.status == Authenticated
&& Belt.Option.isNone(state.access_token)) {
let config: MsalBrowser.acquireTokenSilentConfig = {
scopes,
account: state.account->Belt.Option.getExn,
authority: Some(authority),
claims: None,
forceRefresh: Some(false),
correlationId: None,
};
MsalBrowser.acquireTokenSilent(agentApp, ~config)
|> Js.Promise.then_((accessTokenResponse: MsalBrowser.authResponse) => {
send(SetResponse(accessTokenResponse));
Js.Promise.resolve();
})
|> Js.Promise.catch(e => {
[%log.error "AAD Auth acquireTokenSilent"; ("error", e)];
send(StartAuth);
let loginRequest: MsalBrowser.loginRequest = {
scopes,
redirectUri,
prompt,
state: userState,
authority: Some(authority),
extraQueryParameters,
};
let () = MsalBrowser.loginRedirect(agentApp, ~loginRequest);
Js.Promise.resolve();
})
|> ignore;
};
};
None;
},
(state.account, state.status, state.access_token),
);
let loading =
Belt.Option.getWithDefault(
loadingComponent,
<p> {React.string("Loading")} </p>,
);
switch (state.status, state.id_token, state.id_token_string, state.account) {
| (
Authenticated,
Some(token_claims),
Some(id_token_string),
Some(account),
) =>
let config: MsalBrowser.acquireTokenSilentConfig = {
scopes,
account,
authority: Some(authority),
claims: None,
forceRefresh: Some(true),
correlationId: None,
};
children(
~accessToken=state.access_token,
~token_claims,
~id_token_string,
~account,
~userState=state.userState,
~logout,
~acquireToken=() =>
MsalBrowser.acquireTokenSilent(agentApp, ~config)
|> Js.Promise.then_((accessTokenResponse: MsalBrowser.authResponse) => {
send(SetResponse(accessTokenResponse));
Js.Promise.resolve(accessTokenResponse.idToken);
})
);
| (AuthenticationError(message), _, _, _) =>
[%log.error "AuthenticationError"; ("message", message)];
<p> {React.string(Js.Json.stringify(Obj.magic(message)))} </p>;
| (Authenticated, _, _, _) => loading
| (Authenticating, _, _, _) => loading
| (Unauthenticated, _, _, _) =>
<StartSignIn
onClick={_ => {
send(StartAuth);
let loginRequest: MsalBrowser.loginRequest = {
scopes,
redirectUri,
prompt,
state: userState,
authority: Some(authority),
extraQueryParameters,
};
MsalBrowser.loginRedirect(agentApp, ~loginRequest);
}}
/>
};
};
module IdToken = {
type t = {
[@bs.as "exp"]
expiration: float,
homeObjectId: string,
issuer: string,
name: string,
objectId: string,
preferredName: string,
rawIdToken: string,
subject: string,
[@bs.as "tid"]
tentantId: string,
version: string,
};
};
module Account = {
type t = {
accountIdentifier: string,
homeAccountIdentifier: string,
userName: string,
name: string,
idToken: IdToken.t,
sid: string,
environment: string,
};
};
type logLevel;
type logLevels = {
[@bs.as "Error"]
error: logLevel,
[@bs.as "Info"]
info: logLevel,
[@bs.as "Verbose"]
verbose: logLevel,
[@bs.as "Warning"]
warning: logLevel,
};
[@bs.module "@azure/msal-browser"] external logLevel: logLevels = "LogLevel";
type logger;
type loggerConfig = {
level: logLevel,
correlationId: string,
};
[@bs.new] [@bs.module "@azure/msal-browser"]
external makeLogger: ('cb, loggerConfig) => logger = "Logger";
/* applicationConfig */
type config = {
clientID: string,
graphScopes: array(string),
};
type authCallback =
(
~errorDesc: Js.Nullable.t(string),
~token: string,
~error: string,
~tokenType: string,
~userState: string
) =>
unit;
type cacheLocation = [ | `localStorage | `sessionStorage];
type authResponse = {
uniqueId: string,
tenantId: string,
tokenType: string,
idToken: string,
idTokenClaims: IdToken.t,
accessToken: string,
scopes: array(string),
expiresOn: Js.Date.t,
account: Account.t,
accountState: string,
};
module PublicClientApplication = {
type authConfig = {
clientId: string,
authority: string,
redirectUri: string,
navigateToLoginRequestUrl: bool,
};
type cacheConfig = {
cacheLocation: string,
storeAuthStateInCookie: bool,
};
type config = {
auth: authConfig,
cache: cacheConfig,
};
type publicClientApplication('a) = {
cacheLocation,
loginInProgress: bool,
};
};
[@bs.new] [@bs.module "@azure/msal-browser"]
external makePublicClientApplication:
(~config: PublicClientApplication.config) =>
PublicClientApplication.publicClientApplication('a) =
"PublicClientApplication";
type loginRequest = {
scopes: array(string),
redirectUri: string,
authority: option(string),
state: option(string),
prompt: string,
extraQueryParameters: option(Js.Dict.t(string)),
};
[@bs.send]
external loginPopup:
(
PublicClientApplication.publicClientApplication('a),
~loginRequest: loginRequest
) =>
Js.Promise.t(string) =
"loginPopup";
[@bs.send]
external loginRedirect:
(
PublicClientApplication.publicClientApplication('a),
~loginRequest: loginRequest
) =>
unit =
"loginRedirect";
[@bs.send]
external loginInProgress:
PublicClientApplication.publicClientApplication('a) => bool =
"loginInProgress";
[@bs.send]
external logout:
(PublicClientApplication.publicClientApplication('a), unit) => unit =
"logout";
type acquireTokenSilentConfig = {
account: Account.t,
scopes: array(string),
claims: option(string),
authority: option(string),
forceRefresh: option(bool),
correlationId: option(string),
};
[@bs.send]
external acquireTokenSilent:
(
PublicClientApplication.publicClientApplication('a),
~config: acquireTokenSilentConfig
) =>
Js.Promise.t(authResponse) =
"acquireTokenSilent";
[@bs.send]
external acquireTokenPopup:
(PublicClientApplication.publicClientApplication('a), array(string)) =>
Js.Promise.t(string) =
"aquireTokenPopup";
[@bs.send]
external handleRedirectPromise:
PublicClientApplication.publicClientApplication('a) =>
Js.Promise.t(Js.Nullable.t(authResponse)) =
"handleRedirectPromise";
[@bs.send]
external getAllAccounts:
PublicClientApplication.publicClientApplication('a) => array(Account.t) =
"getAllAccounts";
let internal_getCacheItem = (cacheLocation, itemKey) =>
switch (cacheLocation) {
| `localStorage => Storage.localStorage->(Storage.getItem(itemKey))
| `sessionStorage => Storage.sessionStorage->(Storage.getItem(itemKey))
};
<MsalAuth
clientId="<client_id>"
scopes=[|"openid", "profile", "email"|]
redirectUri={Utils.getRedirectUrl()}
userState={Utils.getCurrentUrl()}
loadingComponent={<LoadingComponent />}
authority="https://login.microsoftonline.com/common"
prompt="select_account">
...{(
~accessToken as _,
~token_claims: MsalBrowser.IdToken.t,
~id_token_string as token,
~account as _,
~userState as _,
~logout,
~acquireToken,
) => {
...
}}
</MsalAuth>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment