AWS Cognito ve Lambda ile kullanıcı yönetimi
İster web, ister mobil, isterse de masaüstü yazılımı olsun; kullanıcının kimliğini doğrulama işlemi hemen her uygulamada gerekli bir altyapıdır. Milyonlarca kullanıcı, kendi uygulamaları için aynı şeyi farklı şekillerde defalarca yeni baştan yazdılar. Bu sebeple bazı standartlar oluştu. Auth0 veya Cognito gibi yapılar bu standartları yönetmek amacıyla oluşturulmuş servislerdir. Daha önce çalıştığım bir şirkette Auth0 kullanmıştım, fakat kendi kişisel projelerimde halihazırda AWS kullandığım için başka bir üçüncü parti modülünü kullanmak istemedim. Bu sebeple Amazon'un Cognito servisini tercih ettim.
Auth0 gerçekten çok güçlü bir şirket ve tamamen kullanıcı yönetimi üzerinde yoğunlaşıyorlar. JWT standardını yaratanlar da Auth0 mühendisleri. Bu sebeple AWS Cognito, fonksiyonellik açısından Auth0'nun yanına bile yaklaşamaz. Ancak, küçük ve orta boyutlu projelerde Auth0'nun pek çok özelliği size lüks gelecektir. Hatta bazı büyük uygulamalarda bile AWS Cognito yeterli olacaktır.
Auth0 için ilk 7000 aylık aktif kullanıcı için bedava iken, AWS Cognito'da bu sayı 50,000'dir. Neyse konumuz AWS Cognito. AWS Cognito ile NodeJS kullanarak kullanıcı oluşturma, email adresini doğrulama ve giriş yapan kullanıcının parolasının nasıl kontrol edileceğini açıklayalım.
Sunucu kullanmadığım için herşeyi AWS Lambda üzerinde gerçekleştiriyorum ancak aynı kodlar elbette(ufak bir modifikasyonla) sunucuda çalışan NodeJS ile de çalışacaktır.
AWS Cognito
Öncelikle AWS Cognito yapılandırmamızı hazırlayalım. Cognito anasayfasında 2 adet seçenek göreceksiniz.
- Manage User Pools
- Manage Federated Identities
Federated Identities kullanıcıların Facebook veya Twitter hesaplarıyla giriş yapıp AWS kaynaklarına(S3, EC2 vs.) erişmelerini sağlayan bir modül. Biz web sayfamız için kullanıcı kontrolü yapmak istiyoruz. Bu yüzden kullanacağımız modül "Manage User Pools".
"Create a User Pool" diyerek yeni bir kullanıcı havuzu oluşturalım. Havuz adını belirttikten sonra "Step Through Settings" ile sihirbazı kullanarak havumuzu adım adım oluşturabiliriz. Aşağıdaki alanlar OAuth standard alanları. Bu alanları zorunlu ya da seçimlik alan olarak belirtebilirsiniz. Bu alanlar dışında eklemek istediğimiz alanlar var ise "Custom" olarak eklenebilir.
OAuth standard özellikleri
- address
- nickname
- birthdate
- phone number
- picture
- family name
- preferred username
- gender
- profile
- given name
- zoneinfo
- locale
- updated at
- middle name
- website
- name
Sonraki adımda kullanıcıların email adresini doğrulamak isteyip istemediğinizi belirtiyorsunuz. Eğer istiyorsanız e-mail mesajının içeriğini de değiştirebiliyorsunuz. Daha sonra "App Clients" kısmında bir ID oluşturmamız gerekiyor. Bu ID önemli. Çünkü daha sonra programımıza hangi havuza bağlanmak istediğimizi bu ID'yi kullanarak belirteceğiz. "App Client Secret" kısmını ise boş bırakıyoruz. Cognito SDK bunu desteklemiyor. Eğer Client Secret yaratırsak Javascript SDK ile kullanamıyoruz. (https://github.com/aws/amazon-cognito-identity-js/issues/2). Bu sanırım güvenlik için özel olarak bu şekilde tasarlanmış. Çünkü Javascript SDK istemci üzerinde çalışabilir. Bu da demek oluyor ki şifreyi herkesin görebileceği şekilde açık etmemiz gerekiyor. Lambda ise sunucu üzerinde çalışıyor elbet, ancak JS SDK kullandığı için bu özelliği kullanamıyoruz. Bu sebeple Lambda'yı yaratırken AWS Cognito kaynaklarına erişebilecek şekilde yaratmamız gerekiyor.
Triggers ya da Analytics gerekli değil. Eğer kullanıcı login olmadan önce, ya da kullanıcı yaratıldıktan sonra vs. gibi durumlar için belli bir kod parçası çalıştırmak istiyorsanız ilgili yerleri kullanabilirsiniz ancak ben gerek duymadım.
Kullanıcı havuzumuzu yarattıktan sonra kodlama işlemine geçebiliriz:
AWS Cognito nedenini bilmediğim bir şekilde Lambda için "aws-sdk" içerisine dahil edilmemiş bu sebeple amazon-cognito-identity.min.js
dosyasını indirip lambda klasörüne eklememiz lazım.
Kullanıcı oluşturma
'use strict';
exports.handler = function(event, context, callback) {
// Burası önemli. Aşağıdaki dosyayı yukarıdaki adresten indirdim
// ve bu dosya ile aynı klasöre kopyaladım
const AWSCognito = require('./amazon-cognito-identity.min');
const payload = JSON.parse(event.body);
const poolData = {
UserPoolId: 'us-east-1_*******',
ClientId: '70do*************ak'
};
const userPool = new AWSCognito.CognitoUserPool(poolData);
let attributeList = [];
const emailAttribute = {
Name: 'email',
Value: payload.email
};
const websiteAttribute = {
Name: 'website',
Value: payload.website
};
const memleketAttribute = {
Name: 'custom:memleket',
Value: payload.memleket
};
attributeList.push(new AWSCognito.CognitoUserAttribute(emailAttribute));
attributeList.push(new AWSCognito.CognitoUserAttribute(websiteAttribute));
attributeList.push(new AWSCognito.CognitoUserAttribute(memleketAttribute));
userPool.signUp(payload.username, payload.password, attributeList, null, function(err, result) {
if (err) {
const errorResponse = httpResponse.getResponse(400, { err });
callback(null, errorResponse);
} else {
const signUpDetails = {
userConfirmed: result.userConfirmed,
userSub: result.userSub,
userName: result.user.username
};
const successResponse = getResponse(200, { signUpDetails });
callback(null, successResponse);
}
});
};
const getResponse = (statusCode, body) => {
const response = {
statusCode: statusCode,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
},
body: JSON.stringify(body)
};
return response;
};
Yukarıda API Gateway kullanarak Lambda'ya POST isteği gönderdik. Bu POST isteğinin içeriğini const payload = JSON.parse(event.body)
ile okuduk. POST içeriğinde ise username, password ikilisinin dışında OAuth standard özellikleri (claims) de bulunabilir. Yukarıdaki örnekte "email" ve"website" özellikleri ile bir de custom "memleket" özelliğini gönderdiğimizi de varsaydık. Daha çok standard ve custom claim eklenebilir. Ancak kodu basit tutmak amacıyla bu iki özelliği ekledim. Bu örnek için payload'ım aşağıdaki gibi:
{
"email": "email@example.com",
"website": "www.example.com",
"memleket": "Giresun"
}
Herhangi bir problem olmadığı taktirde kullanıcı Cognito üzerine kaydediliyor ve eğer email onaylanması istendiyse onay emaili gönderiliyor.
Kullanıcı Onayı
Kullanıcı onayını iki şekilde gerçekleştirebilirsiniz. Eğer
Message Customizations > Verification type
üzerinden "Link" seçeneğini seçerseniz, AWS bu işi kendisi hallediyor. Ancak kendi kullanıcı deneyiminiz sebebiyle "Code" seçeneğini seçerseniz bu işi kendiniz halletmelisiniz. Bu da demek oluyor ki yeni bir gateway endpoint (ya da mevcut endpoint'e yeni bir parametre) ve yeni bir fonksiyon.
Benzer şekilde 'aws-cognito-identitiy' kütüphanesini kullanıyorum
/**
* Enable user status
* Conginot user status is UNCONFIRMED initially
*/
'use strict';
exports.handler = function(event, context, callback) {
const AWSCognito = require('./amazon-cognito-identity.min');
const payload = JSON.parse(event.body);
const confirmationCode = payload.code;
const poolData = {
UserPoolId: 'us-east-1_*********',
ClientId: '70*******************ak'
};
const userPool = new AWSCognito.CognitoUserPool(poolData);
const userData = {
Username: payload.username,
Pool: userPool
};
const cognitoUser = new AWSCognito.CognitoUser(userData);
cognitoUser.confirmRegistration(confirmationCode, true, (err, result) => {
if (err) {
const errorResponse = getResponse(400, err);
callback(null, errorResponse);
return;
}
const successResponse = getResponse(200, result);
callback(null, successResponse);
});
};
const getResponse = (statusCode, body) => {
const response = {
statusCode: statusCode,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
},
body: JSON.stringify(body)
};
return response;
};
Payload'ım ise aşağıdaki gibi
{
"username": "cuneyt",
"code": "ABCDEFGH"
}
Kullanıcı girişi
Kullanıcı girişi kontrolünü ise aşağıdaki gibi yapıyoruz. Elbette normalde kullanıcı girişi yaptıktan sonra kullanıcının onay yaptığını sunucuda Session(oturum) üzerinde tutmamız gerekiyor. Ancak AWS üzerinde tüm istekler tek oturumluk olduğu için bir sonraki istekte kullanıcının giriş yapıp yapmadığını bilemeyeceğiz. Bu nedenle JWT token oluşturmamız gerekiyor fakat bu yazıda sadece Cognito'nun nasıl çalıştığını anlatmak istedim. O yüzden kodumu basit tutuyorum ve sadece kullanıcının onayını yapıyoruz.
'use strict';
exports.handler = function(event, context, callback) {
const AWSCognito = require('./amazon-cognito-identity.min');
const body = JSON.parse(event.body);
const payload = body.payload;
const authenticationData = {
Username: payload.username,
Password: payload.password
};
const authenticationDetails = new AWSCognito.AuthenticationDetails(authenticationData);
const poolData = {
UserPoolId: 'us-east-1_*********',
ClientId: '70*******************ak'
};
const userPool = new AWSCognito.CognitoUserPool(poolData);
const userData = {
Username: payload.username,
Pool: userPool
};
const cognitoUser = new AWSCognito.CognitoUser(userData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function(result) {
const token = { login: true }
const successData = getResponse(200, mySurfToken);
callback(null, successData);
},
onFailure: function(err) {
const failData = getResponse(400, err);
callback(null, failData);
}
});
};
const getResponse = (statusCode, body) => {
const response = {
statusCode: statusCode,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
},
body: JSON.stringify(body)
};
return response;
};
Burada payload'ımız ise:
{
"username": "cuneyt",
"password": "123456"
}
Yukarıdaki 3 özelliğin hemen her online uygulamada bulunması gerekiyor. AWS Cognito bize gerekli minimum fonksiyonelliği sağlıyor. Auth0 gibi uygulamalar çok daha kapsamlı ancak eğer halihazırda AWS kullandığınız bir uygulama var ise Cognito ile çok ucuza hatta büyük ihtimalle (50,000'i aşmayan aktif kullanıcı sayısına sahipseniz) bedavaya bu özelliklere kavuşabiliyorsunuz.