When do you expect add support for SAML Federated Identities?
Cheers,
Saud.
Is it safe to assume that "SAML Federated Identities" means federated with Cognito?
From outside observation, it seems that these federated IdPs are exposed to clients via OpenID Connect, or a conceptually similar flow. If we could get some pointers to details on how SAML Federated IdPs are exposed, I'd be more than happy to take a crack at adding the functionality to Amplify.
It looks like Cognito User Pools expose an OIDC interface over the top of the configured SAML federated IdPs. So, auth is initiated by visiting the /oauth2/authorize
endpoint and providing a set of query string parameters. The two most important ones that I see, that would need explicit configuration for usage, are:
client_id
identity_provider
That latter parameter tells Cognito which federated IdP to route the end user to for AuthN. Everything else about the URL, I think, can be computed from the client environment or Amplify itself.
Am I making sense here?
Another thought here:
Not specific to SAML-federated User Pool users, but a quick way to get an integration win might be to expose a way to register a token refresh handler. Right now, these are added automatically in the constructor for Auth
. When Auth.currentUserCredentials()
is called, it checks to see if you have logged in with a federated provider. If you have, it then checks to see if the token has expired. If it has, it then looks up the refresh handler. If the handler is present, it calls it to get new tokens from the federated IdP before attempting to get new AWS credentials from Cognito.
Now, if Auth
exposed a way to register a handler, then Amplify wouldn't need to worry about implementing any specific logic for arbitrary IdPs. As long as the function provided returned a Promise that resolved to return an object like so:
{
"provider": "my.provider",
"token": "AccessToken",
"user": {},
"expires_at": 123456789
}
then it would hook right in, almost as if the user had explicitly called Auth.federatedSignIn()
_(which itself calls _setCredentialsFromFederation()
)_. That would give control over the flow to the app developer.
Here is the flow. Currently I'm using this with Angular and ADFS. It would be awesome to take out some of my custom code for a more declarative Amplify implementation. Since Angular is a work in progress, maybe they'll come out around the same time.
Any plans to add this into Amplify?
@danieladams456 I am also looking for a sample implementation with Angular and ADFS. Are you able to share your sample? Are you using built-in AWS Cognito Web UI or build your own sign in form and use initiateAuth API?
@stormit-vn, here are some snippets. It is pretty solid but can't handle refreshes. There is some complicated iframe stuff you can do for that type of thing, but I'm not a web developer enough to implement that myself. We did a small app with this, but looking to use Azure AD and custom Lambda authorizers going forward. Doing different roles in AD and passing those in SAML attributes to Cognito was pretty hard and error prone if possible trying to keep those in sync.
I'm using the hosted UI but only enabling ADFS and using the &idp_identifier=
query parameter to select the IDP. See initiateLogin()
method.
@Component({
selector: 'app-oauth-callback',
template: `
<h4>Loading OAuth authorize callback...</h4>
`
})
export class OAuthAuthorizeCallbackComponent implements OnInit {
constructor(private router: Router,
private acitvatedRoute: ActivatedRoute,
private auth: AuthService) { }
ngOnInit() {
const fragmentMap = this.decodeHash(this.acitvatedRoute.snapshot.fragment);
this.auth.updateTokens(fragmentMap);
this.router.navigateByUrl('');
}
// returns empty map if input is null
decodeHash(hashString: string): any {
return !hashString ? {} : hashString.split('&')
.reduce((params, param) => {
const [key, value] = param.split('=');
params[key] = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
return params;
}, {});
}
}
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private auth: AuthService) { }
canActivate(): boolean {
if (!this.auth.isAuthenticated) {
this.auth.initiateLogin();
return false;
}
return true;
}
}
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
constructor(private auth: AuthService, private env: EnvironmentService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.auth.id_token && request.url.indexOf(this.env.values.apiBaseUri) === 0) {
request = request.clone({
setHeaders: {
Authorization: this.auth.id_token
}
});
}
return next.handle(request);
}
}
(local storage is to share the session among different tabs)
// if we want idle timeout, look at
// https://www.npmjs.com/package/@ng-idle/core
@Injectable()
export class AuthService {
private authenticationStatus: BehaviorSubject<AuthenticationStatusModel>;
authenticationStatus$: Observable<AuthenticationStatusModel>;
// tokens for attaching to requests
id_token: string;
expires_at: number;
constructor(private env: EnvironmentService) {
this.authenticationStatus = new BehaviorSubject<AuthenticationStatusModel>(new AuthenticationStatusModel(false));
// load keys if we already have them
this.unpersistKeys();
// let other components get the data now that we're initialized.
this.authenticationStatus$ = this.authenticationStatus.asObservable();
}
get isAuthenticated() {
return this.authenticationStatus.getValue().isAuthenticated;
}
updateTokens(data) {
// validate event
if (!(data.expires_in || data.expires_at) || !data.id_token) {
console.error('invalid update tokens event', data);
this.authenticationStatus.next(new AuthenticationStatusModel(false, null, data.error_description));
return;
}
// figure expiration time, leave 5 minutes buffer
if (data.expires_in) {
this.expires_at = Date.now() + (data.expires_in - 300) * 1000;
} else if (data.expires_at) {
this.expires_at = data.expires_at;
}
// save id token and pull out email
this.id_token = data.id_token;
const email = decode(data.id_token).email;
this.persistKeys();
this.authenticationStatus.next(new AuthenticationStatusModel(true, email));
setTimeout(this.handleTokenTimeout, this.expires_at - Date.now());
}
private persistKeys() {
localStorage.setItem(ID_TOKEN_STORAGE_KEY, JSON.stringify({ id_token: this.id_token, expires_at: this.expires_at }));
}
private unpersistKeys() {
const data = JSON.parse(localStorage.getItem(ID_TOKEN_STORAGE_KEY));
if (!data) {
return;
} else if (!data.id_token || !data.expires_at || data.expires_at < Date.now()) {
localStorage.removeItem(ID_TOKEN_STORAGE_KEY);
} else {
this.updateTokens(data);
}
}
private handleTokenTimeout = () => {
this.authenticationStatus.next(new AuthenticationStatusModel(false, null, 'Session timed out. Please log in again.'));
localStorage.removeItem(ID_TOKEN_STORAGE_KEY);
this.id_token = null;
}
initiateLogin() {
const loginUrl = 'https://' + this.env.values.cognito.subdomain + '.auth.' + this.env.values.cognito.region +
'.amazoncognito.com/oauth2/authorize?scope=openid&response_type=TOKEN&client_id=' + this.env.values.cognito.clientId +
'&redirect_uri=' + this.env.values.appBaseUri + '/oauth2/callback/authorize&idp_identifier=' +
this.env.values.cognito.idpIdentifier;
location.href = loginUrl;
}
initiateLogout() {
localStorage.removeItem(ID_TOKEN_STORAGE_KEY);
const logOutUrl = 'https://' + this.env.values.cognito.subdomain + '.auth.' + this.env.values.cognito.region +
'.amazoncognito.com/logout?client_id=' + this.env.values.cognito.clientId +
'&logout_uri=' + this.env.values.appBaseUri + '/oauth2/callback/logout';
location.href = logOutUrl;
}
}
@danieladams456 actually, we don't need to add a service to parse and handle the token if we add AmplifyService to Angular root module. It will receive and store token to the storage
@danieladams456 with your above code is it possible to get refresh token before expiring access token? can you please share which library you are using if possible full source code please
@BKB503 The SAML flow doesn't supply a refresh token since it does an implicit grant to a public client. You could potentially do some iframe stuff to do a silent refresh in a hidden iframe, but that was too messy for me so not using that anymore due to the 1 hour timeout. To answer your second question - all that code is just doing it manually.
I'm having a React app and an SAML based IdP. Can I use Amplify to create me the flow / app or this feature is not supported yet?
PS: I can't put my user data in Cognito user pool, so I think I only need Cognito Identity Pool (?)
Those looking for sample implementations may find these useful:
It would be great if the Amplify team could make this easier!
Most helpful comment
Any plans to add this into Amplify?