I currently have a system with django which I need to migrate to rails. I am using Devise for authorization in rails. The old django system has it's own set of users which I need to migrate to rails. The thing that I am concerned with, is the password of the users. It is encrypted using sha1 algorithm. So, how I can modify devise such that it is compatible with the old user's password as well.
Each user gets its own random salt, that way if the table with passwords get leaked, rainbow tables wont help to get the actual passwords.
Checkout django/contrib/auth.models.py, check_password(raw_password, enc_password) is what you need to implement in your Rails auth system:
def get_hexdigest(algorithm, salt, raw_password):
"""
Returns a string of the hexdigest of the given plaintext password and salt
using the given algorithm ('md5', 'sha1' or 'crypt').
"""
raw_password, salt = smart_str(raw_password), smart_str(salt)
if algorithm == 'crypt':
try:
import crypt
except ImportError:
raise ValueError('"crypt" password algorithm not supported in this environment')
return crypt.crypt(raw_password, salt)
if algorithm == 'md5':
return md5_constructor(salt + raw_password).hexdigest()
elif algorithm == 'sha1':
return sha_constructor(salt + raw_password).hexdigest()
raise ValueError("Got unknown password algorithm type in password.")
def check_password(raw_password, enc_password):
"""
Returns a boolean of whether the raw_password was correct. Handles
encryption formats behind the scenes.
"""
algo, salt, hsh = enc_password.split('$')
return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password))
I have the following method in my user model:
def valid_password?(pwd)
begin
super(pwd)
rescue
my_pwds = self.encrypted_password.split '$'
Digest::SHA1.hexdigest( my_pwds[1] + pwd ) == my_pwds[2] rescue false
end
end
This extends the default_password? method that is used by Devise to see if a user has submitted the correct password. First the user is being checked using the normal devise logics, and if that doesn't work the Django sha1 logic is run. This way devise passwords are supported too, so you won't get compatibility issues in the future.
Related
Right now we have phone_number as login, and sms code (4 digits) as password.
When user wants to login:
- user enters phone number
- we generate code
- we save code to user password field
- we send code to user via sms
- user uses this sms code to login in
We want to be able to have last 3 generated codes (password) be valid for login:
- we started to save generated codes in separate table
And here is the question: How do I connect this to authlogic? Is the any callback that turns off default password check and give me ability to add my custom logic for password checking?
I found a solution which helped me to tune password validation logic.
My authlogic version 3.5.6 and I has method called validate_by_password in following implementation.
I copied first part of it to save blank fields and logic checks. And overwrote invalid password check in way I need.
class Client::Session < Authlogic::Session::Base
...
def validate_by_password
# copy paste from gem
self.invalid_password = false
# check for blank fields
errors.add(login_field, I18n.t('error_messages.login_blank', default: 'cannot be blank')) if send(login_field).blank?
errors.add(password_field, I18n.t('error_messages.password_blank', default: 'cannot be blank')) if send("protected_#{password_field}").blank?
return if errors.count > 0
# check for unknown login
self.attempted_record = search_for_record(find_by_login_method, send(login_field))
if attempted_record.blank?
generalize_credentials_error_messages? ?
add_general_credentials_error :
errors.add(login_field, I18n.t('error_messages.login_not_found', default: 'is not valid'))
return
end
# custom check for invalid password
...
end
end
I'm new to ruby on rails, and I'm developing an application that will have very sensitive data (api keys from other websites) and I need to store it encrypted in a db but without knowing them at any time.
Let me explain myself:
The form asks the user for his api keys
Encrypt them
Store it in the db
The main question is, how do I encrypt them in such a way that I can use them later (still without knowing them)?
Sorry if the question is silly, but I can't find a way to do it, and thanks.
I've used attr_encrypted for this. Works great.
class User
attr_encrypted :ssn, key: 'This is a key that is 256 bits!!'
end
You then work with ssn as if it were a plain field
user = User.find(1)
puts user.ssn
but it's encrypted at rest (in the database) and can't be retrieved without the key.
def encrypt text
text = text.to_s unless text.is_a? String
len = ActiveSupport::MessageEncryptor.key_len
salt = SecureRandom.hex len
key = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
crypt = ActiveSupport::MessageEncryptor.new key
encrypted_data = crypt.encrypt_and_sign text
"#{salt}$$#{encrypted_data}"
end
def decrypt text
salt, data = text.split "$$"
len = ActiveSupport::MessageEncryptor.key_len
key = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base).generate_key salt, len
crypt = ActiveSupport::MessageEncryptor.new key
crypt.decrypt_and_verify data
end
Pass the key to encrypt method and store the returned encrypted value in DB.
Then to decrypt pass the encrypted key to the decrypt method.
This is assuming your Secret Key Base is in Rails.application.secrets.secret_key_base
The original source for the answer is here
I am using Dart mailer in Flutter and there is a comment that says:
How you use and store passwords is up to you. Beware of storing passwords in plain.
Is there any way to hash the password? How can I avoid storing it in plain text?
It is generally not a good idea to store passwords in plain text anywhere. The way you handle passwords, though, depends on the platform.
Flutter
The flutter_secure_storage package uses Keychain on iOS and KeyStore on Android to store passwords (or tokens).
// Create storage
final storage = FlutterSecureStorage();
// Read secret
String value = await storage.read(key: key);
// Write secret
await storage.write(key: key, value: value);
Note that for Android the min API is 18.
Dart Server
If you are making a server, it is even more important not to store the user passwords in plain text. If the server is compromised, the attacker would have access to all of the passwords, and many users use the same password on multiple accounts.
It would be best to hand the authentication over to Google or Facebook or some other trusted third party by using OAuth2. However, if you are doing your own authorization, you should hash the passwords with a salt and save the hash, not the password itself. This makes it more difficult for an attacker to get the user passwords in case the server is compromised.
A basic implementation (but see comment below) could use the crypto package by the Dart Team.
// import 'package:crypto/crypto.dart';
// import 'dart:convert';
var password = 'password123';
var salt = 'UVocjgjgXg8P7zIsC93kKlRU8sPbTBhsAMFLnLUPDRYFIWAk';
var saltedPassword = salt + password;
var bytes = utf8.encode(saltedPassword);
var hash = sha256.convert(bytes);
Save the salt and the hash. Discard the password. Use a different salt for every user.
To make brute forcing the hashes more difficult, you can also check out the dbcrypt package.
If you want to hash
Use the password_hash package. Their example code is very easy to use:
var generator = new PBKDF2();
var salt = Salt.generateAsBase64String();
var hash = generator.generateKey("mytopsecretpassword", salt, 1000, 32);
Store both the hash and the salt, and you can verify someone else's password attempt by running the generator.generateKey function using their password and the saved salt.
What you actually want
If you're trying to automatically login, you of course need the original password, not a hash. You have a couple options
If the device that will have your app installed is safe, as in it is some company-owned device that has to be logged into by an employee, then have it in plaintext. It doesn't matter. As any company's security policy should be, you must make sure that hard drives are wiped before disposing of electronics (And make sure that no one can stroll in and take the iPad or whatever it is).
If unknown people outside of your organization will be installing your app, you will have to have them login and use their email, or have an API open that will send emails on their behalf (To prevent spamming from your email). The app would sent a POST to that API to send an email. If you had the plaintext password in the application, they could find it on their device, and abuse it.
This response comes late, but here is my approach to storing and using a password for sending emails to recipients using mailer in Flutter. I hope it helps anyone facing this issue.
First I downloaded the crypton package. Then I created a separate dart file where I handle everything related to sending mails, I called it mailer. In this file is where I specify the password, encrypts it using crypton, and use send the email using the decrypted password.
Below is the code of my mailer.dart file:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:mailer/mailer.dart';
import 'package:mailer/smtp_server.dart';
import 'package:intl/intl.dart';
import 'package:crypton/crypton.dart';
class Mailer {
//the variable below we use to encrypt and decrypt the password
RSAKeypair _rsaKeypair = RSAKeypair.fromRandom();
//Below we set the password as a private variable
String _password = 'mySecurePassword';
//We set an encrypted variable that will store the encrypted password;
String _encrypted;
//The function below sets the encrypted variable by assigning it the encrypted value of the password
void setEncrypt () {
_encrypted = _rsaKeypair.publicKey.encrypt(_password);
}
//The function below is responsible for sending the email to the recipients and it is what we call when we want to send an email
emailSender({#required String emailRecipient, #required List<String> paths}) async {
//We call the setEncrypt() function to assign the correct value to the encrypted variable
setEncrypt();
String username = 'email#email.com';
//We asign the decrypted value of encrypted to the password we provide to the smtpServer
String password = _rsaKeypair.privateKey.decrypt(_encrypted);
//The rest of sending an email is the same.
final smtpServer = gmail(username, password);
// Use the SmtpServer class to configure an SMTP server:
// final smtpServer = SmtpServer('smtp.domain.com');
// See the named arguments of SmtpServer for further configuration
// options.
// Create our message.
Message message = Message()
..from = Address(username, 'Your name')
..recipients.add(emailRecipient)
..ccRecipients.addAll(['secondEmail#email.com'])
..subject = 'Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}'
..text = 'This is the plain text.\nThis is line 2 of the text part.'
..html = "<h1>Hi:</h1>\n<p>This is some html</p>\n<p>Greetings, mailer.dart</p>";
for (String path in paths) {
message
..attachments.add(
FileAttachment(
File(
path,
),
),
);
}
var connection = PersistentConnection(smtpServer);
// Send the first message
await connection.send(message);
// send the equivalent message
//await connection.send(equivalentMessage);
// close the connection
await connection.close();
}
}
This was my approach to solving the issue of storing passwords as plain text for sending emails using the mailer package or any package with a similar purpose.
I'm using the devise_token_auth Gem to build a public API.
To destroy a session (sign_out) i have to send : uid (mail), client_id, and access-token (associated with this client_id)
This method from devise_token_auth gem checks if token is still available, and if it is valid. Github code
def token_is_current?(token, client_id)
# ghetto HashWithIndifferentAccess
expiry = self.tokens[client_id]['expiry'] || self.tokens[client_id][:expiry]
token_hash = self.tokens[client_id]['token'] || self.tokens[client_id][:token]
return true if (
# ensure that expiry and token are set
expiry and token and
# ensure that the token has not yet expired
DateTime.strptime(expiry.to_s, '%s') > Time.now and
# ensure that the token is valid
BCrypt::Password.new(token_hash) == token
)
end
I have some issues with this line BCrypt::Password.new(token_hash) == token
What i know is :
token_hash is the token extracted from DB
token came from header of the request
the line is using bcrypt "==" method to compare, which is
def ==(secret);
super(BCrypt::Engine.hash_secret(secret, #salt));
end
Since its using this method to check equality, the check doesn't pass, unless i explicity checking strings values.
Why use Bcrypt to compare two tokens, and not simply compare two strings.
Reading this : bcrypt ruby doc i understand the point of using bcrypt for passwords but why for tokens ?
Google is deprecating the OpenID endpoint I was using (v1.0 I think, via the django_openid_auth module) and I need to update my app and migrate my users' accounts to use Google OAuth2.
I've changed the app to use python-social-auth and have it authenticating with social.backends.google.GoogleOAuth2 successfully.
I've written a pipeline function to find associated OpenID urls from the old table and this is working for the other backends I care about but Google:
def associate_legacy_user(backend, response, uid=None, user=None,
*args, **kwargs):
if uid and not user:
# Try to associate accounts registered in the old openid table
identity_url = None
if backend.name == 'google-oauth2':
# TODO: this isn't working
identity_url = response.get('open_id')
else:
# for all other backends, see if there is a claimed_id url
# matching the identity_url use identity_url instead of uid
# as uid may be the user's email or username
try:
identity_url = response.identity_url
except AttributeError:
identity_url = uid
if identity_url:
# raw sql as this is no longer an installed app
user_ids = sql_query.dbquery('SELECT user_id '
'FROM django_openid_auth_useropenid '
'WHERE claimed_id = %s',
(identity_url,))
if len(user_ids) == 1:
return {'user': User.objects.get(id=user_ids[0]['user_id'])}
As best I can tell from reading Google's migration guide, I need to add an openid.realm to the request, which I've done as follows in settings.py:
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS \
= {'openid.realm': 'http://example.com/'}
But this doesn't seem to be returning the open_id value in the response passed into my pipeline function.
I seem to be stuck on Step 3:
I tried sub-classing the backend to change the RESPONSE_TYPE to add id_token but that returned an empty response:
import social.backends.google
class CustomGoogleOAuth2(social.backends.google.GoogleOAuth2):
RESPONSE_TYPE = 'code id_token'
I tried building an additional request to https://www.googleapis.com/oauth2/v3/token similar to this example, but I don't really know how to go about putting that together and debugging it.
Some more details:
My old claimed_ids for Google OpenID users look like: https://www.google.com/accounts/o8/id?id=AItOawmAW18QuHDdn6PZzaiI5BWUb84mZzNB9eo
I'm happy to use social.backends.google.GoogleOpenIdConnect or a similar alternative backend if that's an easier solution. And while it seems to be closer to what the Google docs are talking about, I wasn't able to get it to work when I tried:
I get a 400 Error: invalid_request Parameter not allowed for this message type: nonce
I can get past the nonce error using social.backends.google.GoogleOpenIdConnect by adding id_token to the RESPONSE_TYPE but then I get an AuthMissingParameter error in my /complete/google-openidconnect/ endpoint as the request's GET and POST are empty. (Tried 'code id_token', 'token id_token', 'id_token', ...)
I don't want to use social.backends.google.GooglePlusAuth as that doesn't integrate as nicely with my current login form.
Worst case, I should be able to use social.pipeline.social_auth.associate_by_email, but I only have email addresses for maybe 80% of the users so that leaves quite a few who will have a new account and need support to associate it manually.
Try as I might, I can't find any examples of people doing a similar migration with python-social-auth, but it must be happening to lots of people.
Any ideas?
Solution works for python social auth 0.1.26
In new versions (0.2.*) of python social auth, there is GoogleOpenIdConnect, but it does not work fine (at least I did not succeed). And my project has some legacy, so I can't use new version of social.
I wrote custom GoogleOpenIdConnect backend:
import datetime
from calendar import timegm
from jwt import InvalidTokenError, decode as jwt_decode
from social.backends.google import GoogleOAuth2
from social.exceptions import AuthTokenError
class GoogleOpenIdConnect(GoogleOAuth2):
name = 'google-openidconnect'
ACCESS_TOKEN_URL = 'https://www.googleapis.com/oauth2/v3/token'
DEFAULT_SCOPE = ['openid']
EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')]
ID_TOKEN_ISSUER = "accounts.google.com"
def user_data(self, access_token, *args, **kwargs):
return self.get_json(
'https://www.googleapis.com/plus/v1/people/me/openIdConnect',
params={'access_token': access_token, 'alt': 'json'}
)
def get_user_id(self, details, response):
return response['sub']
def request_access_token(self, *args, **kwargs):
"""
Retrieve the access token. Also, validate the id_token and
store it (temporarily).
"""
response = self.get_json(*args, **kwargs)
response['id_token_parsed'] = self.validate_and_return_id_token(response['id_token'])
return response
def validate_and_return_id_token(self, id_token):
"""
Validates the id_token according to the steps at
http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.
"""
try:
id_token = jwt_decode(id_token, verify=False)
except InvalidTokenError as err:
raise AuthTokenError(self, err)
# Verify the token was issued in the last 10 minutes
utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
if id_token['iat'] < (utc_timestamp - 600):
raise AuthTokenError(self, 'Incorrect id_token: iat')
return id_token
Notes:
get_user_id – An identifier for the user, unique among all Google accounts and never reused.
request_access_token – there is I add id_token_parsed to response, and it will be used in pipeline.
validate_and_return_id_token – validate of jwt is disabled, because in google developers console I have registered Client ID as web application so, I have no certificates for validate this data.
Then I created pipelines:
def social_user_google_backwards(strategy, uid, *args, **kwargs):
"""
Provide find user that was connect with google openID, but is logging with google oauth2
"""
result = social_user(strategy, uid, *args, **kwargs)
provider = strategy.backend.name
user = result.get('user')
if provider != 'google-openidconnect' or user is not None:
return result
openid_id = kwargs.get('response', {}).get('id_token_parsed', {}).get('openid_id')
if openid_id is None:
return result
social = _get_google_openid(strategy, openid_id)
if social is not None:
result.update({
'user': social.user,
'is_new': social.user is None,
'google_openid_social': social
})
return result
def _get_google_openid(strategy, openid_id):
social = strategy.storage.user.get_social_auth('openid', openid_id)
if social:
return social
return None
def associate_user(strategy, uid, user=None, social=None, *args, **kwargs):
result = social_associate_user(strategy, uid, user, social, *args, **kwargs)
google_openid_social = kwargs.pop('google_openid_social', None)
if google_openid_social is not None:
google_openid_social.delete()
return result
And changed my SOCIAL_AUTH_PIPELINE and AUTHENTICATION_BACKENDS settings:
AUTHENTICATION_BACKENDS = (
...
#'social.backends.open_id.OpenIdAuth' remove it
'social_extension.backends.google.GoogleOpenIdConnect', # add it
...
)
and
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
# 'social.pipeline.social_auth.social_user', remove it
'social_extension.pipeline.social_user_google_backwards', # add it
'social.pipeline.user.get_username',
...
# 'social.pipeline.social_auth.associate_user', remove it
'social_extension.pipeline.associate_user', # add it
'social.pipeline.social_auth.load_extra_data',
...
)